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

Commit 78cc340c authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Offer to stream and fsync() install sessions.

Installers are interested in both streaming APK data and establishing
a happens-after relationship to support resuming downloads after a
process kill or battery pull.

This exposes a generic OutputStream for writing, and hooks up flush()
to be a blocking call which returns only when all outstanding write()
data has been fsync()'ed to disk.

Tests to verify behavior.

Bug: 14975160
Change-Id: I38289867c80ac659163bb0c2158ef12d99cc570d
parent 05ad4820
Loading
Loading
Loading
Loading
+13 −3
Original line number Diff line number Diff line
@@ -19,9 +19,12 @@ package android.content.pm;
import android.app.PackageInstallObserver;
import android.app.PackageUninstallObserver;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.FileBridge;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;

import java.io.OutputStream;

/** {@hide} */
public class PackageInstaller {
    private final PackageManager mPm;
@@ -127,10 +130,17 @@ public class PackageInstaller {
            }
        }

        public ParcelFileDescriptor openWrite(String overlayName, long offsetBytes,
                long lengthBytes) {
        /**
         * Open an APK file for writing, starting at the given offset. You can
         * then stream data into the file, periodically calling
         * {@link OutputStream#flush()} to ensure bytes have been written to
         * disk.
         */
        public OutputStream openWrite(String splitName, long offsetBytes, long lengthBytes) {
            try {
                return mSession.openWrite(overlayName, offsetBytes, lengthBytes);
                final ParcelFileDescriptor clientSocket = mSession.openWrite(splitName,
                        offsetBytes, lengthBytes);
                return new FileBridge.FileBridgeOutputStream(clientSocket.getFileDescriptor());
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            }
+165 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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 android.os;

import static android.system.OsConstants.AF_UNIX;
import static android.system.OsConstants.SOCK_STREAM;

import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;

import libcore.io.IoBridge;
import libcore.io.IoUtils;
import libcore.io.Memory;
import libcore.io.Streams;

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.OutputStream;
import java.io.SyncFailedException;
import java.nio.ByteOrder;
import java.util.Arrays;

/**
 * Simple bridge that allows file access across process boundaries without
 * returning the underlying {@link FileDescriptor}. This is useful when the
 * server side needs to strongly assert that a client side is completely
 * hands-off.
 *
 * @hide
 */
public class FileBridge extends Thread {
    private static final String TAG = "FileBridge";

    // TODO: consider extending to support bidirectional IO

    private static final int MSG_LENGTH = 8;

    /** CMD_WRITE [len] [data] */
    private static final int CMD_WRITE = 1;
    /** CMD_FSYNC */
    private static final int CMD_FSYNC = 2;

    private FileDescriptor mTarget;

    private final FileDescriptor mServer = new FileDescriptor();
    private final FileDescriptor mClient = new FileDescriptor();

    private volatile boolean mClosed;

    public FileBridge() {
        try {
            Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mServer, mClient);
        } catch (ErrnoException e) {
            throw new RuntimeException("Failed to create bridge");
        }
    }

    public boolean isClosed() {
        return mClosed;
    }

    public void setTargetFile(FileDescriptor target) {
        mTarget = target;
    }

    public FileDescriptor getClientSocket() {
        return mClient;
    }

    @Override
    public void run() {
        final byte[] temp = new byte[8192];
        try {
            while (IoBridge.read(mServer, temp, 0, MSG_LENGTH) == MSG_LENGTH) {
                final int cmd = Memory.peekInt(temp, 0, ByteOrder.BIG_ENDIAN);

                if (cmd == CMD_WRITE) {
                    // Shuttle data into local file
                    int len = Memory.peekInt(temp, 4, ByteOrder.BIG_ENDIAN);
                    while (len > 0) {
                        int n = IoBridge.read(mServer, temp, 0, Math.min(temp.length, len));
                        IoBridge.write(mTarget, temp, 0, n);
                        len -= n;
                    }

                } else if (cmd == CMD_FSYNC) {
                    // Sync and echo back to confirm
                    Os.fsync(mTarget);
                    IoBridge.write(mServer, temp, 0, MSG_LENGTH);
                }
            }

            // Client was closed; one last fsync
            Os.fsync(mTarget);

        } catch (ErrnoException e) {
            Log.e(TAG, "Failed during bridge: ", e);
        } catch (IOException e) {
            Log.e(TAG, "Failed during bridge: ", e);
        } finally {
            IoUtils.closeQuietly(mTarget);
            IoUtils.closeQuietly(mServer);
            IoUtils.closeQuietly(mClient);
            mClosed = true;
        }
    }

    public static class FileBridgeOutputStream extends OutputStream {
        private final FileDescriptor mClient;
        private final byte[] mTemp = new byte[MSG_LENGTH];

        public FileBridgeOutputStream(FileDescriptor client) {
            mClient = client;
        }

        @Override
        public void close() throws IOException {
            IoBridge.closeAndSignalBlockedThreads(mClient);
        }

        @Override
        public void flush() throws IOException {
            Memory.pokeInt(mTemp, 0, CMD_FSYNC, ByteOrder.BIG_ENDIAN);
            IoBridge.write(mClient, mTemp, 0, MSG_LENGTH);

            // Wait for server to ack
            if (IoBridge.read(mClient, mTemp, 0, MSG_LENGTH) == MSG_LENGTH) {
                if (Memory.peekInt(mTemp, 0, ByteOrder.BIG_ENDIAN) == CMD_FSYNC) {
                    return;
                }
            }

            throw new SyncFailedException("Failed to fsync() across bridge");
        }

        @Override
        public void write(byte[] buffer, int byteOffset, int byteCount) throws IOException {
            Arrays.checkOffsetAndCount(buffer.length, byteOffset, byteCount);
            Memory.pokeInt(mTemp, 0, CMD_WRITE, ByteOrder.BIG_ENDIAN);
            Memory.pokeInt(mTemp, 4, byteCount, ByteOrder.BIG_ENDIAN);
            IoBridge.write(mClient, mTemp, 0, MSG_LENGTH);
            IoBridge.write(mClient, buffer, byteOffset, byteCount);
        }

        @Override
        public void write(int oneByte) throws IOException {
            Streams.writeSingleByte(this, oneByte);
        }
    }
}
+156 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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 android.os;

import android.os.FileBridge.FileBridgeOutputStream;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;

import libcore.io.Streams;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Random;

public class FileBridgeTest extends AndroidTestCase {

    private File file;
    private FileOutputStream fileOs;
    private FileBridge bridge;
    private FileBridgeOutputStream client;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        file = getContext().getFileStreamPath("meow.dat");
        file.delete();

        fileOs = new FileOutputStream(file);

        bridge = new FileBridge();
        bridge.setTargetFile(fileOs.getFD());
        bridge.start();
        client = new FileBridgeOutputStream(bridge.getClientSocket());
    }

    @Override
    protected void tearDown() throws Exception {
        fileOs.close();
        file.delete();
    }

    private void assertOpen() throws Exception {
        assertFalse("expected open", bridge.isClosed());
    }

    private void closeAndAssertClosed() throws Exception {
        client.close();

        // Wait a beat for things to settle down
        SystemClock.sleep(200);
        assertTrue("expected closed", bridge.isClosed());
    }

    private void assertContents(byte[] expected) throws Exception {
        MoreAsserts.assertEquals(expected, Streams.readFully(new FileInputStream(file)));
    }

    public void testNoWriteNoSync() throws Exception {
        assertOpen();
        closeAndAssertClosed();
    }

    public void testNoWriteSync() throws Exception {
        assertOpen();
        client.flush();
        closeAndAssertClosed();
    }

    public void testWriteNoSync() throws Exception {
        assertOpen();
        client.write("meow".getBytes(StandardCharsets.UTF_8));
        closeAndAssertClosed();
        assertContents("meow".getBytes(StandardCharsets.UTF_8));
    }

    public void testWriteSync() throws Exception {
        assertOpen();
        client.write("cake".getBytes(StandardCharsets.UTF_8));
        client.flush();
        closeAndAssertClosed();
        assertContents("cake".getBytes(StandardCharsets.UTF_8));
    }

    public void testWriteSyncWrite() throws Exception {
        assertOpen();
        client.write("meow".getBytes(StandardCharsets.UTF_8));
        client.flush();
        client.write("cake".getBytes(StandardCharsets.UTF_8));
        closeAndAssertClosed();
        assertContents("meowcake".getBytes(StandardCharsets.UTF_8));
    }

    public void testEmptyWrite() throws Exception {
        assertOpen();
        client.write(new byte[0]);
        closeAndAssertClosed();
        assertContents(new byte[0]);
    }

    public void testWriteAfterClose() throws Exception {
        assertOpen();
        client.write("meow".getBytes(StandardCharsets.UTF_8));
        closeAndAssertClosed();
        try {
            client.write("cake".getBytes(StandardCharsets.UTF_8));
            fail("wrote after close!");
        } catch (IOException expected) {
        }
        assertContents("meow".getBytes(StandardCharsets.UTF_8));
    }

    public void testRandomWrite() throws Exception {
        final Random r = new Random();
        final ByteArrayOutputStream result = new ByteArrayOutputStream();

        for (int i = 0; i < 512; i++) {
            final byte[] test = new byte[r.nextInt(24169)];
            r.nextBytes(test);
            result.write(test);
            client.write(test);
            client.flush();
        }

        closeAndAssertClosed();
        assertContents(result.toByteArray());
    }

    public void testGiantWrite() throws Exception {
        final byte[] test = new byte[263401];
        new Random().nextBytes(test);

        assertOpen();
        client.write(test);
        closeAndAssertClosed();
        assertContents(test);
    }
}
+10 −55
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import android.content.pm.PackageParser.PackageLite;
import android.content.pm.Signature;
import android.os.Build;
import android.os.Bundle;
import android.os.FileBridge;
import android.os.FileUtils;
import android.os.Handler;
import android.os.Looper;
@@ -114,7 +115,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
    private boolean mPermissionsConfirmed;
    private boolean mInvalid;

    private ArrayList<WritePipe> mPipes = new ArrayList<>();
    private ArrayList<FileBridge> mBridges = new ArrayList<>();

    private IPackageInstallObserver2 mRemoteObserver;

@@ -159,14 +160,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        // Quick sanity check of state, and allocate a pipe for ourselves. We
        // then do heavy disk allocation outside the lock, but this open pipe
        // will block any attempted install transitions.
        final WritePipe pipe;
        final FileBridge bridge;
        synchronized (mLock) {
            if (!mMutationsAllowed) {
                throw new IllegalStateException("Mutations not allowed");
            }

            pipe = new WritePipe();
            mPipes.add(pipe);
            bridge = new FileBridge();
            mBridges.add(bridge);
        }

        try {
@@ -194,9 +195,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
                Libcore.os.lseek(targetFd, offsetBytes, OsConstants.SEEK_SET);
            }

            pipe.setTargetFd(targetFd);
            pipe.start();
            return pipe.getWriteFd();
            bridge.setTargetFile(targetFd);
            bridge.start();
            return new ParcelFileDescriptor(bridge.getClientSocket());

        } catch (ErrnoException e) {
            throw new IllegalStateException("Failed to write", e);
@@ -218,8 +219,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {

        // Verify that all writers are hands-off
        if (mMutationsAllowed) {
            for (WritePipe pipe : mPipes) {
                if (!pipe.isClosed()) {
            for (FileBridge bridge : mBridges) {
                if (!bridge.isClosed()) {
                    throw new InstallFailedException(INSTALL_FAILED_PACKAGE_CHANGED,
                            "Files still open");
                }
@@ -482,52 +483,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
        }
    }

    private static class WritePipe extends Thread {
        private final ParcelFileDescriptor[] mPipe;

        private FileDescriptor mTargetFd;

        private volatile boolean mClosed;

        public WritePipe() {
            try {
                mPipe = ParcelFileDescriptor.createPipe();
            } catch (IOException e) {
                throw new IllegalStateException("Failed to create pipe");
            }
        }

        public boolean isClosed() {
            return mClosed;
        }

        public void setTargetFd(FileDescriptor targetFd) {
            mTargetFd = targetFd;
        }

        public ParcelFileDescriptor getWriteFd() {
            return mPipe[1];
        }

        @Override
        public void run() {
            FileInputStream in = null;
            FileOutputStream out = null;
            try {
                // TODO: look at switching to sendfile(2) to speed up
                in = new FileInputStream(mPipe[0].getFileDescriptor());
                out = new FileOutputStream(mTargetFd);
                Streams.copy(in, out);
            } catch (IOException e) {
                Slog.w(TAG, "Failed to stream data: " + e);
            } finally {
                IoUtils.closeQuietly(mPipe[0]);
                IoUtils.closeQuietly(mTargetFd);
                mClosed = true;
            }
        }
    }

    private class InstallFailedException extends Exception {
        private final int error;