Merge "Offer to stream and fsync() install sessions."
This commit is contained in:
@@ -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
core/java/android/os/FileBridge.java
Normal file
165
core/java/android/os/FileBridge.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
156
core/tests/coretests/src/android/os/FileBridgeTest.java
Normal file
156
core/tests/coretests/src/android/os/FileBridgeTest.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user