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

Commit 93ea2212 authored by Kenny Root's avatar Kenny Root Committed by android-build-merger
Browse files

Merge "Make RecoverySystemService more testable" am: 49af39e7

am: 1b506101

Change-Id: Iae3938bb6cb9703e79bc0836b8e85f82de025c26
parents eb24ef66 1b506101
Loading
Loading
Loading
Loading
+323 −193
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.os.RemoteException;
import android.os.SystemProperties;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemService;

import libcore.io.IoUtils;
@@ -35,6 +36,7 @@ import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * The recovery system service is responsible for coordinating recovery related
@@ -43,7 +45,7 @@ import java.io.IOException;
 * triggers /system/bin/uncrypt via init to de-encrypt an OTA package on the
 * /data partition so that it can be accessed under the recovery image.
 */
public final class RecoverySystemService extends SystemService {
public class RecoverySystemService extends IRecoverySystem.Stub {
    private static final String TAG = "RecoverySystemService";
    private static final boolean DEBUG = false;

@@ -51,27 +53,94 @@ public final class RecoverySystemService extends SystemService {
    private static final String UNCRYPT_SOCKET = "uncrypt";

    // The init services that communicate with /system/bin/uncrypt.
    private static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt";
    private static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb";
    private static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb";
    @VisibleForTesting
    static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt";
    @VisibleForTesting
    static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb";
    @VisibleForTesting
    static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb";

    private static final Object sRequestLock = new Object();

    private static final int SOCKET_CONNECTION_MAX_RETRY = 30;

    private static final Object sRequestLock = new Object();
    private final Injector mInjector;
    private final Context mContext;

    private Context mContext;
    static class Injector {
        protected final Context mContext;

    public RecoverySystemService(Context context) {
        super(context);
        Injector(Context context) {
            mContext = context;
        }

        public Context getContext() {
            return mContext;
        }

        public PowerManager getPowerManager() {
            return (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
        }

        public String systemPropertiesGet(String key) {
            return SystemProperties.get(key);
        }

        public void systemPropertiesSet(String key, String value) {
            SystemProperties.set(key, value);
        }

        public boolean uncryptPackageFileDelete() {
            return RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
        }

        public String getUncryptPackageFileName() {
            return RecoverySystem.UNCRYPT_PACKAGE_FILE.getName();
        }

        public FileWriter getUncryptPackageFileWriter() throws IOException {
            return new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE);
        }

        public UncryptSocket connectService() {
            UncryptSocket socket = new UncryptSocket();
            if (!socket.connectService()) {
                socket.close();
                return null;
            }
            return socket;
        }

        public void threadSleep(long millis) throws InterruptedException {
            Thread.sleep(millis);
        }
    }

    /**
     * Handles the lifecycle events for the RecoverySystemService.
     */
    public static final class Lifecycle extends SystemService {
        public Lifecycle(Context context) {
            super(context);
        }

        @Override
        public void onStart() {
        publishBinderService(Context.RECOVERY_SERVICE, new BinderService());
            RecoverySystemService recoverySystemService = new RecoverySystemService(getContext());
            publishBinderService(Context.RECOVERY_SERVICE, recoverySystemService);
        }
    }

    private RecoverySystemService(Context context) {
        this(new Injector(context));
    }

    @VisibleForTesting
    RecoverySystemService(Injector injector) {
        mInjector = injector;
        mContext = injector.getContext();
    }

    private final class BinderService extends IRecoverySystem.Stub {
    @Override // Binder call
    public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) {
        if (DEBUG) Slog.d(TAG, "uncrypt: " + filename);
@@ -79,43 +148,38 @@ public final class RecoverySystemService extends SystemService {
        synchronized (sRequestLock) {
            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);

                final boolean available = checkAndWaitForUncryptService();
                if (!available) {
            if (!checkAndWaitForUncryptService()) {
                Slog.e(TAG, "uncrypt service is unavailable.");
                return false;
            }

                // Write the filename into UNCRYPT_PACKAGE_FILE to be read by
            // Write the filename into uncrypt package file to be read by
            // uncrypt.
                RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
            mInjector.uncryptPackageFileDelete();

                try (FileWriter uncryptFile = new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE)) {
            try (FileWriter uncryptFile = mInjector.getUncryptPackageFileWriter()) {
                uncryptFile.write(filename + "\n");
            } catch (IOException e) {
                    Slog.e(TAG, "IOException when writing \"" +
                            RecoverySystem.UNCRYPT_PACKAGE_FILE + "\":", e);
                Slog.e(TAG, "IOException when writing \""
                        + mInjector.getUncryptPackageFileName() + "\":", e);
                return false;
            }

            // Trigger uncrypt via init.
                SystemProperties.set("ctl.start", "uncrypt");
            mInjector.systemPropertiesSet("ctl.start", "uncrypt");

            // Connect to the uncrypt service socket.
                LocalSocket socket = connectService();
            UncryptSocket socket = mInjector.connectService();
            if (socket == null) {
                Slog.e(TAG, "Failed to connect to uncrypt socket");
                return false;
            }

            // Read the status from the socket.
                DataInputStream dis = null;
                DataOutputStream dos = null;
            try {
                    dis = new DataInputStream(socket.getInputStream());
                    dos = new DataOutputStream(socket.getOutputStream());
                int lastStatus = Integer.MIN_VALUE;
                while (true) {
                        int status = dis.readInt();
                    int status = socket.getPercentageUncrypted();
                    // Avoid flooding the log with the same message.
                    if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
                        continue;
@@ -137,7 +201,7 @@ public final class RecoverySystemService extends SystemService {
                            // Ack receipt of the final status code. uncrypt
                            // waits for the ack so the socket won't be
                            // destroyed before we receive the code.
                                dos.writeInt(0);
                            socket.sendAck();
                            break;
                        }
                    } else {
@@ -146,7 +210,7 @@ public final class RecoverySystemService extends SystemService {
                        // Ack receipt of the final status code. uncrypt waits
                        // for the ack so the socket won't be destroyed before
                        // we receive the code.
                            dos.writeInt(0);
                        socket.sendAck();
                        return false;
                    }
                }
@@ -154,9 +218,7 @@ public final class RecoverySystemService extends SystemService {
                Slog.e(TAG, "IOException when reading status: ", e);
                return false;
            } finally {
                    IoUtils.closeQuietly(dis);
                    IoUtils.closeQuietly(dos);
                    IoUtils.closeQuietly(socket);
                socket.close();
            }

            return true;
@@ -188,7 +250,7 @@ public final class RecoverySystemService extends SystemService {
            }

            // Having set up the BCB, go ahead and reboot.
                PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
            PowerManager pm = mInjector.getPowerManager();
            pm.reboot(PowerManager.REBOOT_RECOVERY);
        }
    }
@@ -201,16 +263,16 @@ public final class RecoverySystemService extends SystemService {
     */
    private boolean checkAndWaitForUncryptService() {
        for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
                final String uncryptService = SystemProperties.get(INIT_SERVICE_UNCRYPT);
                final String setupBcbService = SystemProperties.get(INIT_SERVICE_SETUP_BCB);
                final String clearBcbService = SystemProperties.get(INIT_SERVICE_CLEAR_BCB);
                final boolean busy = "running".equals(uncryptService) ||
                        "running".equals(setupBcbService) || "running".equals(clearBcbService);
            final String uncryptService = mInjector.systemPropertiesGet(INIT_SERVICE_UNCRYPT);
            final String setupBcbService = mInjector.systemPropertiesGet(INIT_SERVICE_SETUP_BCB);
            final String clearBcbService = mInjector.systemPropertiesGet(INIT_SERVICE_CLEAR_BCB);
            final boolean busy = "running".equals(uncryptService)
                    || "running".equals(setupBcbService) || "running".equals(clearBcbService);
            if (DEBUG) {
                    Slog.i(TAG, "retry: " + retry + " busy: " + busy +
                            " uncrypt: [" + uncryptService + "]" +
                            " setupBcb: [" + setupBcbService + "]" +
                            " clearBcb: [" + clearBcbService + "]");
                Slog.i(TAG, "retry: " + retry + " busy: " + busy
                        + " uncrypt: [" + uncryptService + "]"
                        + " setupBcb: [" + setupBcbService + "]"
                        + " clearBcb: [" + clearBcbService + "]");
            }

            if (!busy) {
@@ -218,7 +280,7 @@ public final class RecoverySystemService extends SystemService {
            }

            try {
                    Thread.sleep(1000);
                mInjector.threadSleep(1000);
            } catch (InterruptedException e) {
                Slog.w(TAG, "Interrupted:", e);
            }
@@ -227,33 +289,6 @@ public final class RecoverySystemService extends SystemService {
        return false;
    }

        private LocalSocket connectService() {
            LocalSocket socket = new LocalSocket();
            boolean done = false;
            // The uncrypt socket will be created by init upon receiving the
            // service request. It may not be ready by this point. So we will
            // keep retrying until success or reaching timeout.
            for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
                try {
                    socket.connect(new LocalSocketAddress(UNCRYPT_SOCKET,
                            LocalSocketAddress.Namespace.RESERVED));
                    done = true;
                    break;
                } catch (IOException ignored) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        Slog.w(TAG, "Interrupted:", e);
                    }
                }
            }
            if (!done) {
                Slog.e(TAG, "Timed out connecting to uncrypt socket");
                return null;
            }
            return socket;
        }

    private boolean setupOrClearBcb(boolean isSetup, String command) {
        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);

@@ -264,41 +299,34 @@ public final class RecoverySystemService extends SystemService {
        }

        if (isSetup) {
                SystemProperties.set("ctl.start", "setup-bcb");
            mInjector.systemPropertiesSet("ctl.start", "setup-bcb");
        } else {
                SystemProperties.set("ctl.start", "clear-bcb");
            mInjector.systemPropertiesSet("ctl.start", "clear-bcb");
        }

        // Connect to the uncrypt service socket.
            LocalSocket socket = connectService();
        UncryptSocket socket = mInjector.connectService();
        if (socket == null) {
            Slog.e(TAG, "Failed to connect to uncrypt socket");
            return false;
        }

            DataInputStream dis = null;
            DataOutputStream dos = null;
        try {
                dis = new DataInputStream(socket.getInputStream());
                dos = new DataOutputStream(socket.getOutputStream());

            // Send the BCB commands if it's to setup BCB.
            if (isSetup) {
                    byte[] cmdUtf8 = command.getBytes("UTF-8");
                    dos.writeInt(cmdUtf8.length);
                    dos.write(cmdUtf8, 0, cmdUtf8.length);
                socket.sendCommand(command);
            }

            // Read the status from the socket.
                int status = dis.readInt();
            int status = socket.getPercentageUncrypted();

            // Ack receipt of the status code. uncrypt waits for the ack so
            // the socket won't be destroyed before we receive the code.
                dos.writeInt(0);
            socket.sendAck();

            if (status == 100) {
                    Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear") +
                            " bcb successfully finished.");
                Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear")
                        + " bcb successfully finished.");
            } else {
                // Error in /system/bin/uncrypt.
                Slog.e(TAG, "uncrypt failed with status: " + status);
@@ -308,12 +336,114 @@ public final class RecoverySystemService extends SystemService {
            Slog.e(TAG, "IOException when communicating with uncrypt:", e);
            return false;
        } finally {
                IoUtils.closeQuietly(dis);
                IoUtils.closeQuietly(dos);
                IoUtils.closeQuietly(socket);
            socket.close();
        }

        return true;
    }

    /**
     * Provides a wrapper for the low-level details of framing packets sent to the uncrypt
     * socket.
     */
    public static class UncryptSocket {
        private LocalSocket mLocalSocket;
        private DataInputStream mInputStream;
        private DataOutputStream mOutputStream;

        /**
         * Attempt to connect to the uncrypt service. Connection will be retried for up to
         * {@link #SOCKET_CONNECTION_MAX_RETRY} times. If the connection is unsuccessful, the
         * socket will be closed. If the connection is successful, the connection must be closed
         * by the caller.
         *
         * @return true if connection was successful, false if unsuccessful
         */
        public boolean connectService() {
            mLocalSocket = new LocalSocket();
            boolean done = false;
            // The uncrypt socket will be created by init upon receiving the
            // service request. It may not be ready by this point. So we will
            // keep retrying until success or reaching timeout.
            for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
                try {
                    mLocalSocket.connect(new LocalSocketAddress(UNCRYPT_SOCKET,
                            LocalSocketAddress.Namespace.RESERVED));
                    done = true;
                    break;
                } catch (IOException ignored) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        Slog.w(TAG, "Interrupted:", e);
                    }
                }
            }
            if (!done) {
                Slog.e(TAG, "Timed out connecting to uncrypt socket");
                close();
                return false;
            }

            try {
                mInputStream = new DataInputStream(mLocalSocket.getInputStream());
                mOutputStream = new DataOutputStream(mLocalSocket.getOutputStream());
            } catch (IOException e) {
                close();
                return false;
            }

            return true;
        }

        /**
         * Sends a command to the uncrypt service.
         *
         * @param command command to send to the uncrypt service
         * @throws IOException if the socket is closed or there was an error writing to the socket
         */
        public void sendCommand(String command) throws IOException {
            if (mLocalSocket.isClosed()) {
                throw new IOException("socket is closed");
            }

            byte[] cmdUtf8 = command.getBytes(StandardCharsets.UTF_8);
            mOutputStream.writeInt(cmdUtf8.length);
            mOutputStream.write(cmdUtf8, 0, cmdUtf8.length);
        }

        /**
         * Reads the status from the uncrypt service which is usually represented as a percentage.
         * @return an integer representing the percentage completed
         * @throws IOException if the socket was closed or there was an error reading the socket
         */
        public int getPercentageUncrypted() throws IOException {
            if (mLocalSocket.isClosed()) {
                throw new IOException("socket is closed");
            }

            return mInputStream.readInt();
        }

        /**
         * Sends a confirmation to the uncrypt service.
         * @throws IOException if the socket was closed or there was an error writing to the socket
         */
        public void sendAck() throws IOException {
            if (mLocalSocket.isClosed()) {
                throw new IOException("socket is closed");
            }

            mOutputStream.writeInt(0);
        }

        /**
         * Closes the socket and all underlying data streams.
         */
        public void close() {
            IoUtils.closeQuietly(mInputStream);
            IoUtils.closeQuietly(mOutputStream);
            IoUtils.closeQuietly(mLocalSocket);
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -701,7 +701,7 @@ public final class SystemServer {

        // Bring up recovery system in case a rescue party needs a reboot
        traceBeginAndSlog("StartRecoverySystemService");
        mSystemServiceManager.startService(RecoverySystemService.class);
        mSystemServiceManager.startService(RecoverySystemService.Lifecycle.class);
        traceEnd();

        // Now that we have the bare essentials of the OS up and running, take
+197 −0

File added.

Preview size limit exceeded, changes collapsed.

+113 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.server.recoverysystem;

import android.content.Context;
import android.os.PowerManager;

import java.io.FileWriter;

public class RecoverySystemServiceTestable extends RecoverySystemService {
    private static class MockInjector extends RecoverySystemService.Injector {
        private final FakeSystemProperties mSystemProperties;
        private final PowerManager mPowerManager;
        private final FileWriter mUncryptPackageFileWriter;
        private final UncryptSocket mUncryptSocket;

        MockInjector(Context context, FakeSystemProperties systemProperties,
                PowerManager powerManager, FileWriter uncryptPackageFileWriter,
                UncryptSocket uncryptSocket) {
            super(context);
            mSystemProperties = systemProperties;
            mPowerManager = powerManager;
            mUncryptPackageFileWriter = uncryptPackageFileWriter;
            mUncryptSocket = uncryptSocket;
        }

        @Override
        public PowerManager getPowerManager() {
            return mPowerManager;
        }

        @Override
        public String systemPropertiesGet(String key) {
            return mSystemProperties.get(key);
        }

        @Override
        public void systemPropertiesSet(String key, String value) {
            mSystemProperties.set(key, value);
        }

        @Override
        public boolean uncryptPackageFileDelete() {
            return true;
        }

        @Override
        public String getUncryptPackageFileName() {
            return "mock-file.txt";
        }

        @Override
        public FileWriter getUncryptPackageFileWriter() {
            return mUncryptPackageFileWriter;
        }

        @Override
        public UncryptSocket connectService() {
            return mUncryptSocket;
        }

        @Override
        public void threadSleep(long millis) {
        }
    }

    RecoverySystemServiceTestable(Context context, FakeSystemProperties systemProperties,
            PowerManager powerManager, FileWriter uncryptPackageFileWriter,
            UncryptSocket uncryptSocket) {
        super(new MockInjector(context, systemProperties, powerManager, uncryptPackageFileWriter,
                uncryptSocket));
    }

    public static class FakeSystemProperties {
        private String mCtlStart = null;

        public String get(String key) {
            if (RecoverySystemService.INIT_SERVICE_UNCRYPT.equals(key)
                    || RecoverySystemService.INIT_SERVICE_SETUP_BCB.equals(key)
                    || RecoverySystemService.INIT_SERVICE_CLEAR_BCB.equals(key)) {
                return null;
            } else {
                throw new IllegalArgumentException("unexpected test key: " + key);
            }
        }

        public void set(String key, String value) {
            if ("ctl.start".equals(key)) {
                mCtlStart = value;
            } else {
                throw new IllegalArgumentException("unexpected test key: " + key);
            }
        }

        public String getCtlStart() {
            return mCtlStart;
        }
    }
}