Merge "Offer to stream and fsync() install sessions."
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index d7bd473..4672015 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -19,9 +19,12 @@
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 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();
}
diff --git a/core/java/android/os/FileBridge.java b/core/java/android/os/FileBridge.java
new file mode 100644
index 0000000..7f8bc9f
--- /dev/null
+++ b/core/java/android/os/FileBridge.java
@@ -0,0 +1,165 @@
+/*
+ * 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);
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/os/FileBridgeTest.java b/core/tests/coretests/src/android/os/FileBridgeTest.java
new file mode 100644
index 0000000..d4f6b1f
--- /dev/null
+++ b/core/tests/coretests/src/android/os/FileBridgeTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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);
+ }
+}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index f90d7ab..3ed73f7 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -32,6 +32,7 @@
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 @@
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 @@
// 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 @@
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 @@
// 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 @@
}
}
- 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;