Merge changes from topic "blob-perf-compute-digest"

* changes:
  Add perf test to measure duration of blob store digest computation.
  Add verbose logging that can be eanbled to debug issues.
  Schedule a maintenance job to clean up stale jobs.
This commit is contained in:
Sudheer Shanka
2020-02-06 16:22:52 +00:00
committed by Android (Google) Code Review
20 changed files with 1354 additions and 214 deletions

View File

@@ -0,0 +1,28 @@
// Copyright (C) 2020 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.
android_test {
name: "BlobStorePerfTests",
srcs: ["src/**/*.java"],
static_libs: [
"BlobStoreTestUtils",
"androidx.test.rules",
"androidx.annotation_annotation",
"apct-perftests-utils",
"ub-uiautomator",
],
platform_apis: true,
test_suites: ["device-tests"],
certificate: "platform",
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.perftests.blob">
<application>
<uses-library android:name="android.test.runner" />
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.perftests.blob"/>
</manifest>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 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.
-->
<configuration description="Runs BlobStorePerfTests metric instrumentation.">
<option name="test-suite-tag" value="apct" />
<option name="test-suite-tag" value="apct-metric-instrumentation" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="BlobStorePerfTests.apk" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.perftests.blob" />
<option name="hidden-api-checks" value="false"/>
</test>
</configuration>

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2020 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.perftests.blob;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.os.ParcelFileDescriptor;
import android.perftests.utils.TraceMarkParser;
import android.perftests.utils.TraceMarkParser.TraceMarkSlice;
import android.support.test.uiautomator.UiDevice;
import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.function.BiConsumer;
// Copy of com.android.frameworks.perftests.am.util.AtraceUtils. TODO: avoid this duplication.
public class AtraceUtils {
private static final String TAG = "AtraceUtils";
private static final boolean VERBOSE = true;
private static final String ATRACE_START = "atrace --async_start -b %d -c %s";
private static final String ATRACE_DUMP = "atrace --async_dump";
private static final String ATRACE_STOP = "atrace --async_stop";
private static final int DEFAULT_ATRACE_BUF_SIZE = 1024;
private UiAutomation mAutomation;
private static AtraceUtils sUtils = null;
private boolean mStarted = false;
private AtraceUtils(Instrumentation instrumentation) {
mAutomation = instrumentation.getUiAutomation();
}
public static AtraceUtils getInstance(Instrumentation instrumentation) {
if (sUtils == null) {
sUtils = new AtraceUtils(instrumentation);
}
return sUtils;
}
/**
* @param categories The list of the categories to trace, separated with space.
*/
public void startTrace(String categories) {
synchronized (this) {
if (mStarted) {
throw new IllegalStateException("atrace already started");
}
runShellCommand(String.format(
ATRACE_START, DEFAULT_ATRACE_BUF_SIZE, categories));
mStarted = true;
}
}
public void stopTrace() {
synchronized (this) {
mStarted = false;
runShellCommand(ATRACE_STOP);
}
}
private String runShellCommand(String cmd) {
try {
return UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @param parser The function that can accept the buffer of atrace dump and parse it.
* @param handler The parse result handler
*/
public void performDump(TraceMarkParser parser,
BiConsumer<String, List<TraceMarkSlice>> handler) {
parser.reset();
try {
if (VERBOSE) {
Log.i(TAG, "Collecting atrace dump...");
}
writeDataToBuf(mAutomation.executeShellCommand(ATRACE_DUMP), parser);
} catch (IOException e) {
Log.e(TAG, "Error in reading dump", e);
}
parser.forAllSlices(handler);
}
// The given file descriptor here will be closed by this function
private void writeDataToBuf(ParcelFileDescriptor pfDescriptor,
TraceMarkParser parser) throws IOException {
InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfDescriptor);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
parser.visit(line);
}
}
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2020 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.perftests.blob;
import android.app.blob.BlobStoreManager;
import android.content.Context;
import android.perftests.utils.ManualBenchmarkState;
import android.perftests.utils.PerfManualStatusReporter;
import android.perftests.utils.TraceMarkParser;
import android.perftests.utils.TraceMarkParser.TraceMarkSlice;
import android.support.test.uiautomator.UiDevice;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.utils.blob.DummyBlobData;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@LargeTest
@RunWith(Parameterized.class)
public class BlobStorePerfTests {
// From frameworks/native/cmds/atrace/atrace.cpp
private static final String ATRACE_CATEGORY_SYSTEM_SERVER = "ss";
// From f/b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
private static final String ATRACE_COMPUTE_DIGEST_PREFIX = "computeBlobDigest-";
private Context mContext;
private BlobStoreManager mBlobStoreManager;
private AtraceUtils mAtraceUtils;
private ManualBenchmarkState mState;
@Rule
public PerfManualStatusReporter mPerfManualStatusReporter = new PerfManualStatusReporter();
@Parameterized.Parameter(0)
public int fileSizeInMb;
@Parameterized.Parameters(name = "{0}MB")
public static Collection<Object[]> getParameters() {
return Arrays.asList(new Object[][] {
{ 25 },
{ 50 },
{ 100 },
{ 200 },
});
}
@Before
public void setUp() {
mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
mBlobStoreManager = (BlobStoreManager) mContext.getSystemService(
Context.BLOB_STORE_SERVICE);
mAtraceUtils = AtraceUtils.getInstance(InstrumentationRegistry.getInstrumentation());
mState = mPerfManualStatusReporter.getBenchmarkState();
}
@After
public void tearDown() {
// TODO: Add a blob_store shell command to trigger idle maintenance to avoid hardcoding
// job id like this.
// From BlobStoreConfig.IDLE_JOB_ID = 191934935.
runShellCommand("cmd jobscheduler run -f android 191934935");
}
@Test
public void testComputeDigest() throws Exception {
mAtraceUtils.startTrace(ATRACE_CATEGORY_SYSTEM_SERVER);
try {
final List<Long> durations = new ArrayList<>();
final DummyBlobData blobData = prepareDataBlob(fileSizeInMb);
final TraceMarkParser parser = new TraceMarkParser(
line -> line.name.startsWith(ATRACE_COMPUTE_DIGEST_PREFIX));
while (mState.keepRunning(durations)) {
commitBlob(blobData);
durations.clear();
collectDigestDurationsFromTrace(parser, durations);
// get and delete blobId
}
} finally {
mAtraceUtils.stopTrace();
}
}
private void collectDigestDurationsFromTrace(TraceMarkParser parser, List<Long> durations) {
mAtraceUtils.performDump(parser, (key, slices) -> {
for (TraceMarkSlice slice : slices) {
durations.add(TimeUnit.MICROSECONDS.toNanos(slice.getDurationInMicroseconds()));
}
});
}
private DummyBlobData prepareDataBlob(int fileSizeInMb) throws Exception {
final DummyBlobData blobData = new DummyBlobData(mContext,
fileSizeInMb * 1024 * 1024 /* bytes */);
blobData.prepare();
return blobData;
}
private void commitBlob(DummyBlobData blobData) throws Exception {
final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
blobData.writeToSession(session);
final CompletableFuture<Integer> callback = new CompletableFuture<>();
session.commit(mContext.getMainExecutor(), callback::complete);
// Ignore commit callback result.
callback.get();
}
}
private String runShellCommand(String cmd) {
try {
return UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -4,7 +4,7 @@
"name": "CtsBlobStoreTestCases"
},
{
"name": "FrameworksServicesTests",
"name": "FrameworksMockingServicesTests",
"options": [
{
"include-filter": "com.android.server.blob"

View File

@@ -257,6 +257,11 @@ public final class BlobHandle implements Parcelable {
return Base64.encodeToString(digest, Base64.NO_WRAP);
}
/** @hide */
public boolean isExpired() {
return expiryTimeMillis != 0 && expiryTimeMillis < System.currentTimeMillis();
}
public static final @NonNull Creator<BlobHandle> CREATOR = new Creator<BlobHandle>() {
@Override
public @NonNull BlobHandle createFromParcel(@NonNull Parcel source) {

View File

@@ -64,9 +64,9 @@ class BlobMetadata {
private final Context mContext;
public final long blobId;
public final BlobHandle blobHandle;
public final int userId;
private final long mBlobId;
private final BlobHandle mBlobHandle;
private final int mUserId;
@GuardedBy("mMetadataLock")
private final ArraySet<Committer> mCommitters = new ArraySet<>();
@@ -90,9 +90,21 @@ class BlobMetadata {
BlobMetadata(Context context, long blobId, BlobHandle blobHandle, int userId) {
mContext = context;
this.blobId = blobId;
this.blobHandle = blobHandle;
this.userId = userId;
this.mBlobId = blobId;
this.mBlobHandle = blobHandle;
this.mUserId = userId;
}
long getBlobId() {
return mBlobId;
}
BlobHandle getBlobHandle() {
return mBlobHandle;
}
int getUserId() {
return mUserId;
}
void addCommitter(@NonNull Committer committer) {
@@ -159,7 +171,7 @@ class BlobMetadata {
boolean hasLeases() {
synchronized (mMetadataLock) {
return mLeasees.isEmpty();
return !mLeasees.isEmpty();
}
}
@@ -196,7 +208,7 @@ class BlobMetadata {
File getBlobFile() {
if (mBlobFile == null) {
mBlobFile = BlobStoreConfig.getBlobFile(blobId);
mBlobFile = BlobStoreConfig.getBlobFile(mBlobId);
}
return mBlobFile;
}
@@ -244,7 +256,7 @@ class BlobMetadata {
void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) {
fout.println("blobHandle:");
fout.increaseIndent();
blobHandle.dump(fout, dumpArgs.shouldDumpFull());
mBlobHandle.dump(fout, dumpArgs.shouldDumpFull());
fout.decreaseIndent();
fout.println("Committers:");
@@ -274,11 +286,11 @@ class BlobMetadata {
void writeToXml(XmlSerializer out) throws IOException {
synchronized (mMetadataLock) {
XmlUtils.writeLongAttribute(out, ATTR_ID, blobId);
XmlUtils.writeIntAttribute(out, ATTR_USER_ID, userId);
XmlUtils.writeLongAttribute(out, ATTR_ID, mBlobId);
XmlUtils.writeIntAttribute(out, ATTR_USER_ID, mUserId);
out.startTag(null, TAG_BLOB_HANDLE);
blobHandle.writeToXml(out);
mBlobHandle.writeToXml(out);
out.endTag(null, TAG_BLOB_HANDLE);
for (int i = 0, count = mCommitters.size(); i < count; ++i) {

View File

@@ -18,12 +18,15 @@ package com.android.server.blob;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Environment;
import android.util.Log;
import android.util.Slog;
import java.io.File;
import java.util.concurrent.TimeUnit;
class BlobStoreConfig {
public static final String TAG = "BlobStore";
public static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
public static final int CURRENT_XML_VERSION = 1;
@@ -32,6 +35,20 @@ class BlobStoreConfig {
private static final String SESSIONS_INDEX_FILE_NAME = "sessions_index.xml";
private static final String BLOBS_INDEX_FILE_NAME = "blobs_index.xml";
/**
* Job Id for idle maintenance job ({@link BlobStoreIdleJobService}).
*/
public static final int IDLE_JOB_ID = 0xB70B1D7; // 191934935L
/**
* Max time period (in millis) between each idle maintenance job run.
*/
public static final long IDLE_JOB_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
/**
* Timeout in millis after which sessions with no updates will be deleted.
*/
public static final long SESSION_EXPIRY_TIMEOUT_MILLIS = TimeUnit.DAYS.toMillis(7);
@Nullable
public static File prepareBlobFile(long sessionId) {
final File blobsDir = prepareBlobsDir();

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2020 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.blob;
import static com.android.server.blob.BlobStoreConfig.IDLE_JOB_ID;
import static com.android.server.blob.BlobStoreConfig.IDLE_JOB_PERIOD_MILLIS;
import static com.android.server.blob.BlobStoreConfig.LOGV;
import static com.android.server.blob.BlobStoreConfig.TAG;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Slog;
import com.android.server.LocalServices;
/**
* Maintenance job to clean up stale sessions and blobs.
*/
public class BlobStoreIdleJobService extends JobService {
@Override
public boolean onStartJob(final JobParameters params) {
AsyncTask.execute(() -> {
final BlobStoreManagerInternal blobStoreManagerInternal = LocalServices.getService(
BlobStoreManagerInternal.class);
blobStoreManagerInternal.onIdleMaintenance();
jobFinished(params, false);
});
return false;
}
@Override
public boolean onStopJob(final JobParameters params) {
Slog.d(TAG, "Idle maintenance job is stopped; id=" + params.getJobId()
+ ", reason=" + JobParameters.getReasonCodeDescription(params.getStopReason()));
return false;
}
static void schedule(Context context) {
final JobScheduler jobScheduler = (JobScheduler) context.getSystemService(
Context.JOB_SCHEDULER_SERVICE);
final JobInfo job = new JobInfo.Builder(IDLE_JOB_ID,
new ComponentName(context, BlobStoreIdleJobService.class))
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.setPeriodic(IDLE_JOB_PERIOD_MILLIS)
.build();
jobScheduler.schedule(job);
if (LOGV) {
Slog.v(TAG, "Scheduling the idle maintenance job");
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2020 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.blob;
/**
* BlobStoreManager local system service interface.
*
* Only for use within the system server.
*/
public abstract class BlobStoreManagerInternal {
/**
* Triggered from idle maintenance job to cleanup stale blobs and sessions.
*/
public abstract void onIdleMaintenance();
}

View File

@@ -28,6 +28,8 @@ import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
import static android.os.UserHandle.USER_NULL;
import static com.android.server.blob.BlobStoreConfig.CURRENT_XML_VERSION;
import static com.android.server.blob.BlobStoreConfig.LOGV;
import static com.android.server.blob.BlobStoreConfig.SESSION_EXPIRY_TIMEOUT_MILLIS;
import static com.android.server.blob.BlobStoreConfig.TAG;
import static com.android.server.blob.BlobStoreSession.STATE_ABANDONED;
import static com.android.server.blob.BlobStoreSession.STATE_COMMITTED;
@@ -61,6 +63,7 @@ import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManagerInternal;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.ExceptionUtils;
import android.util.LongSparseArray;
@@ -94,8 +97,10 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Service responsible for maintaining and facilitating access to data blobs published by apps.
@@ -115,6 +120,10 @@ public class BlobStoreManagerService extends SystemService {
@GuardedBy("mBlobsLock")
private final SparseArray<ArrayMap<BlobHandle, BlobMetadata>> mBlobsMap = new SparseArray<>();
// Contains all ids that are currently in use.
@GuardedBy("mBlobsLock")
private final ArraySet<Long> mKnownBlobIds = new ArraySet<>();
private final Context mContext;
private final Handler mHandler;
private final Injector mInjector;
@@ -151,6 +160,7 @@ public class BlobStoreManagerService extends SystemService {
@Override
public void onStart() {
publishBinderService(Context.BLOB_STORE_SERVICE, new Stub());
LocalServices.addService(BlobStoreManagerInternal.class, new LocalService());
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
registerReceivers();
@@ -164,6 +174,8 @@ public class BlobStoreManagerService extends SystemService {
readBlobSessionsLocked(allPackages);
readBlobsInfoLocked(allPackages);
}
} else if (phase == PHASE_BOOT_COMPLETED) {
BlobStoreIdleJobService.schedule(mContext);
}
}
@@ -215,6 +227,40 @@ public class BlobStoreManagerService extends SystemService {
}
}
@VisibleForTesting
void addKnownIdsForTest(long... knownIds) {
synchronized (mBlobsLock) {
for (long id : knownIds) {
mKnownBlobIds.add(id);
}
}
}
@VisibleForTesting
Set<Long> getKnownIdsForTest() {
synchronized (mBlobsLock) {
return mKnownBlobIds;
}
}
@GuardedBy("mBlobsLock")
private void addSessionForUserLocked(BlobStoreSession session, int userId) {
getUserSessionsLocked(userId).put(session.getSessionId(), session);
mKnownBlobIds.add(session.getSessionId());
}
@GuardedBy("mBlobsLock")
private void addBlobForUserLocked(BlobMetadata blobMetadata, int userId) {
addBlobForUserLocked(blobMetadata, getUserBlobsLocked(userId));
}
@GuardedBy("mBlobsLock")
private void addBlobForUserLocked(BlobMetadata blobMetadata,
ArrayMap<BlobHandle, BlobMetadata> userBlobs) {
userBlobs.put(blobMetadata.getBlobHandle(), blobMetadata);
mKnownBlobIds.add(blobMetadata.getBlobId());
}
private long createSessionInternal(BlobHandle blobHandle,
int callingUid, String callingPackage) {
synchronized (mBlobsLock) {
@@ -223,7 +269,11 @@ public class BlobStoreManagerService extends SystemService {
final BlobStoreSession session = new BlobStoreSession(mContext,
sessionId, blobHandle, callingUid, callingPackage,
mSessionStateChangeListener);
getUserSessionsLocked(UserHandle.getUserId(callingUid)).put(sessionId, session);
addSessionForUserLocked(session, UserHandle.getUserId(callingUid));
if (LOGV) {
Slog.v(TAG, "Created session for " + blobHandle
+ "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
}
writeBlobSessionsAsync();
return sessionId;
}
@@ -251,7 +301,10 @@ public class BlobStoreManagerService extends SystemService {
callingUid, callingPackage);
session.open();
session.abandon();
if (LOGV) {
Slog.v(TAG, "Deleted session with id " + sessionId
+ "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
}
writeBlobSessionsAsync();
}
}
@@ -286,6 +339,10 @@ public class BlobStoreManagerService extends SystemService {
}
blobMetadata.addLeasee(callingPackage, callingUid,
descriptionResId, leaseExpiryTimeMillis);
if (LOGV) {
Slog.v(TAG, "Acquired lease on " + blobHandle
+ "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
}
writeBlobsInfoAsync();
}
}
@@ -301,6 +358,10 @@ public class BlobStoreManagerService extends SystemService {
+ "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
}
blobMetadata.removeLeasee(callingPackage, callingUid);
if (LOGV) {
Slog.v(TAG, "Released lease on " + blobHandle
+ "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
}
writeBlobsInfoAsync();
}
}
@@ -329,6 +390,10 @@ public class BlobStoreManagerService extends SystemService {
session.getSessionFile().delete();
getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid()))
.remove(session.getSessionId());
mKnownBlobIds.remove(session.getSessionId());
if (LOGV) {
Slog.v(TAG, "Session is invalid; deleted " + session);
}
break;
case STATE_COMMITTED:
session.verifyBlobData();
@@ -340,7 +405,7 @@ public class BlobStoreManagerService extends SystemService {
if (blob == null) {
blob = new BlobMetadata(mContext,
session.getSessionId(), session.getBlobHandle(), userId);
userBlobs.put(session.getBlobHandle(), blob);
addBlobForUserLocked(blob, userBlobs);
}
final Committer newCommitter = new Committer(session.getOwnerPackageName(),
session.getOwnerUid(), session.getBlobAccessMode());
@@ -355,6 +420,9 @@ public class BlobStoreManagerService extends SystemService {
}
getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid()))
.remove(session.getSessionId());
if (LOGV) {
Slog.v(TAG, "Successfully committed session " + session);
}
break;
default:
Slog.wtf(TAG, "Invalid session state: "
@@ -397,6 +465,9 @@ public class BlobStoreManagerService extends SystemService {
out.endTag(null, TAG_SESSIONS);
out.endDocument();
sessionsIndexFile.finishWrite(fos);
if (LOGV) {
Slog.v(TAG, "Finished persisting sessions data");
}
} catch (Exception e) {
sessionsIndexFile.failWrite(fos);
Slog.wtf(TAG, "Error writing sessions data", e);
@@ -437,8 +508,8 @@ public class BlobStoreManagerService extends SystemService {
if (userPackages != null
&& session.getOwnerPackageName().equals(
userPackages.get(session.getOwnerUid()))) {
getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid())).put(
session.getSessionId(), session);
addSessionForUserLocked(session,
UserHandle.getUserId(session.getOwnerUid()));
} else {
// Unknown package or the session data does not belong to this package.
session.getSessionFile().delete();
@@ -446,6 +517,9 @@ public class BlobStoreManagerService extends SystemService {
mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, session.getSessionId());
}
}
if (LOGV) {
Slog.v(TAG, "Finished reading sessions data");
}
} catch (Exception e) {
Slog.wtf(TAG, "Error reading sessions data", e);
}
@@ -479,6 +553,9 @@ public class BlobStoreManagerService extends SystemService {
out.endTag(null, TAG_BLOBS);
out.endDocument();
blobsIndexFile.finishWrite(fos);
if (LOGV) {
Slog.v(TAG, "Finished persisting blobs data");
}
} catch (Exception e) {
blobsIndexFile.failWrite(fos);
Slog.wtf(TAG, "Error writing blobs data", e);
@@ -510,18 +587,21 @@ public class BlobStoreManagerService extends SystemService {
if (TAG_BLOB.equals(in.getName())) {
final BlobMetadata blobMetadata = BlobMetadata.createFromXml(mContext, in);
final SparseArray<String> userPackages = allPackages.get(blobMetadata.userId);
final SparseArray<String> userPackages = allPackages.get(
blobMetadata.getUserId());
if (userPackages == null) {
blobMetadata.getBlobFile().delete();
} else {
getUserBlobsLocked(blobMetadata.userId).put(
blobMetadata.blobHandle, blobMetadata);
addBlobForUserLocked(blobMetadata, blobMetadata.getUserId());
blobMetadata.removeInvalidCommitters(userPackages);
blobMetadata.removeInvalidLeasees(userPackages);
}
mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, blobMetadata.blobId);
mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, blobMetadata.getBlobId());
}
}
if (LOGV) {
Slog.v(TAG, "Finished reading blobs data");
}
} catch (Exception e) {
Slog.wtf(TAG, "Error reading blobs data", e);
}
@@ -614,6 +694,7 @@ public class BlobStoreManagerService extends SystemService {
if (session.getOwnerUid() == uid
&& session.getOwnerPackageName().equals(packageName)) {
session.getSessionFile().delete();
mKnownBlobIds.remove(session.getSessionId());
indicesToRemove.add(i);
}
}
@@ -633,6 +714,7 @@ public class BlobStoreManagerService extends SystemService {
// Delete the blob if it doesn't have any active leases.
if (!blobMetadata.hasLeases()) {
blobMetadata.getBlobFile().delete();
mKnownBlobIds.remove(blobMetadata.getBlobId());
indicesToRemove.add(i);
}
}
@@ -640,6 +722,10 @@ public class BlobStoreManagerService extends SystemService {
userBlobs.removeAt(indicesToRemove.get(i));
}
writeBlobsInfoAsync();
if (LOGV) {
Slog.v(TAG, "Removed blobs data associated with pkg="
+ packageName + ", uid=" + uid);
}
}
}
@@ -651,6 +737,7 @@ public class BlobStoreManagerService extends SystemService {
for (int i = 0, count = userSessions.size(); i < count; ++i) {
final BlobStoreSession session = userSessions.valueAt(i);
session.getSessionFile().delete();
mKnownBlobIds.remove(session.getSessionId());
}
}
@@ -660,11 +747,107 @@ public class BlobStoreManagerService extends SystemService {
for (int i = 0, count = userBlobs.size(); i < count; ++i) {
final BlobMetadata blobMetadata = userBlobs.valueAt(i);
blobMetadata.getBlobFile().delete();
mKnownBlobIds.remove(blobMetadata.getBlobId());
}
}
if (LOGV) {
Slog.v(TAG, "Removed blobs data in user " + userId);
}
}
}
@GuardedBy("mBlobsLock")
@VisibleForTesting
void handleIdleMaintenanceLocked() {
// Cleanup any left over data on disk that is not part of index.
final ArrayList<Long> deletedBlobIds = new ArrayList<>();
final ArrayList<File> filesToDelete = new ArrayList<>();
final File blobsDir = BlobStoreConfig.getBlobsDir();
if (blobsDir.exists()) {
for (File file : blobsDir.listFiles()) {
try {
final long id = Long.parseLong(file.getName());
if (mKnownBlobIds.indexOf(id) < 0) {
filesToDelete.add(file);
deletedBlobIds.add(id);
}
} catch (NumberFormatException e) {
Slog.wtf(TAG, "Error parsing the file name: " + file, e);
filesToDelete.add(file);
}
}
for (int i = 0, count = filesToDelete.size(); i < count; ++i) {
filesToDelete.get(i).delete();
}
}
// Cleanup any stale blobs.
for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) {
final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i);
userBlobs.entrySet().removeIf(entry -> {
final BlobHandle blobHandle = entry.getKey();
final BlobMetadata blobMetadata = entry.getValue();
boolean shouldRemove = false;
// Cleanup expired data blobs.
if (blobHandle.isExpired()) {
shouldRemove = true;
}
// Cleanup blobs with no active leases.
// TODO: Exclude blobs which were just committed.
if (!blobMetadata.hasLeases()) {
shouldRemove = true;
}
if (shouldRemove) {
blobMetadata.getBlobFile().delete();
mKnownBlobIds.remove(blobMetadata.getBlobId());
deletedBlobIds.add(blobMetadata.getBlobId());
}
return shouldRemove;
});
}
writeBlobsInfoAsync();
// Cleanup any stale sessions.
final ArrayList<Integer> indicesToRemove = new ArrayList<>();
for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) {
final LongSparseArray<BlobStoreSession> userSessions = mSessions.valueAt(i);
indicesToRemove.clear();
for (int j = 0, sessionsCount = userSessions.size(); j < sessionsCount; ++j) {
final BlobStoreSession blobStoreSession = userSessions.valueAt(j);
boolean shouldRemove = false;
// Cleanup sessions which haven't been modified in a while.
if (blobStoreSession.getSessionFile().lastModified()
< System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS) {
shouldRemove = true;
}
// Cleanup sessions with already expired data.
if (blobStoreSession.getBlobHandle().isExpired()) {
shouldRemove = true;
}
if (shouldRemove) {
blobStoreSession.getSessionFile().delete();
mKnownBlobIds.remove(blobStoreSession.getSessionId());
indicesToRemove.add(j);
deletedBlobIds.add(blobStoreSession.getSessionId());
}
}
for (int j = 0; j < indicesToRemove.size(); ++j) {
userSessions.removeAt(indicesToRemove.get(j));
}
}
if (LOGV) {
Slog.v(TAG, "Completed idle maintenance; deleted "
+ Arrays.toString(deletedBlobIds.toArray()));
}
writeBlobSessionsAsync();
}
void runClearAllSessions(@UserIdInt int userId) {
synchronized (mBlobsLock) {
if (userId == UserHandle.USER_ALL) {
@@ -727,10 +910,10 @@ public class BlobStoreManagerService extends SystemService {
fout.increaseIndent();
for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) {
final BlobMetadata blobMetadata = userBlobs.valueAt(j);
if (!dumpArgs.shouldDumpBlob(blobMetadata.blobId)) {
if (!dumpArgs.shouldDumpBlob(blobMetadata.getBlobId())) {
continue;
}
fout.println("Blob #" + blobMetadata.blobId);
fout.println("Blob #" + blobMetadata.getBlobId());
fout.increaseIndent();
blobMetadata.dump(fout, dumpArgs);
fout.decreaseIndent();
@@ -742,6 +925,9 @@ public class BlobStoreManagerService extends SystemService {
private class PackageChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (LOGV) {
Slog.v(TAG, "Received " + intent);
}
switch (intent.getAction()) {
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
case Intent.ACTION_PACKAGE_DATA_CLEARED:
@@ -893,6 +1079,14 @@ public class BlobStoreManagerService extends SystemService {
final DumpArgs dumpArgs = DumpArgs.parse(args);
final IndentingPrintWriter fout = new IndentingPrintWriter(writer, " ");
if (dumpArgs.shouldDumpHelp()) {
writer.println("dumpsys blob_store [options]:");
fout.increaseIndent();
dumpArgs.dumpArgsUsage(fout);
fout.decreaseIndent();
return;
}
synchronized (mBlobsLock) {
fout.println("mCurrentMaxSessionId: " + mCurrentMaxSessionId);
fout.println();
@@ -926,6 +1120,7 @@ public class BlobStoreManagerService extends SystemService {
private boolean mDumpOnlySelectedSections;
private boolean mDumpSessions;
private boolean mDumpBlobs;
private boolean mDumpHelp;
public boolean shouldDumpSession(String packageName, int uid, long blobId) {
if (!CollectionUtils.isEmpty(mDumpPackages)
@@ -971,6 +1166,10 @@ public class BlobStoreManagerService extends SystemService {
|| mDumpUserIds.indexOf(userId) >= 0;
}
public boolean shouldDumpHelp() {
return mDumpHelp;
}
private DumpArgs() {}
public static DumpArgs parse(String[] args) {
@@ -1000,6 +1199,8 @@ public class BlobStoreManagerService extends SystemService {
dumpArgs.mDumpUserIds.add(getIntArgRequired(args, ++i, "userId"));
} else if ("--blob".equals(opt) || "-b".equals(opt)) {
dumpArgs.mDumpBlobIds.add(getLongArgRequired(args, ++i, "blobId"));
} else if ("--help".equals(opt) || "-h".equals(opt)) {
dumpArgs.mDumpHelp = true;
} else {
// Everything else is assumed to be blob ids.
dumpArgs.mDumpBlobIds.add(getLongArgRequired(args, i, "blobId"));
@@ -1040,6 +1241,40 @@ public class BlobStoreManagerService extends SystemService {
}
return value;
}
private void dumpArgsUsage(IndentingPrintWriter pw) {
pw.println("--help | -h");
printWithIndent(pw, "Dump this help text");
pw.println("--sessions");
printWithIndent(pw, "Dump only the sessions info");
pw.println("--blobs");
printWithIndent(pw, "Dump only the committed blobs info");
pw.println("--package | -p [package-name]");
printWithIndent(pw, "Dump blobs info associated with the given package");
pw.println("--uid | -u [uid]");
printWithIndent(pw, "Dump blobs info associated with the given uid");
pw.println("--user [user-id]");
printWithIndent(pw, "Dump blobs info in the given user");
pw.println("--blob | -b [session-id | blob-id]");
printWithIndent(pw, "Dump blob info corresponding to the given ID");
pw.println("--full | -f");
printWithIndent(pw, "Dump full unredacted blobs data");
}
private void printWithIndent(IndentingPrintWriter pw, String str) {
pw.increaseIndent();
pw.println(str);
pw.decreaseIndent();
}
}
private class LocalService extends BlobStoreManagerInternal {
@Override
public void onIdleMaintenance() {
synchronized (mBlobsLock) {
handleIdleMaintenanceLocked();
}
}
}
@VisibleForTesting

View File

@@ -21,11 +21,13 @@ import static android.app.blob.XmlTags.ATTR_PACKAGE;
import static android.app.blob.XmlTags.ATTR_UID;
import static android.app.blob.XmlTags.TAG_ACCESS_MODE;
import static android.app.blob.XmlTags.TAG_BLOB_HANDLE;
import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER;
import static android.system.OsConstants.O_CREAT;
import static android.system.OsConstants.O_RDONLY;
import static android.system.OsConstants.O_RDWR;
import static android.system.OsConstants.SEEK_SET;
import static com.android.server.blob.BlobStoreConfig.LOGV;
import static com.android.server.blob.BlobStoreConfig.TAG;
import android.annotation.BytesLong;
@@ -40,6 +42,7 @@ import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.RevocableFileDescriptor;
import android.os.Trace;
import android.os.storage.StorageManager;
import android.system.ErrnoException;
import android.system.Os;
@@ -381,15 +384,22 @@ class BlobStoreSession extends IBlobStoreSession.Stub {
void verifyBlobData() {
byte[] actualDigest = null;
try {
Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER,
"computeBlobDigest-i" + mSessionId + "-l" + getSessionFile().length());
actualDigest = FileUtils.digest(getSessionFile(), mBlobHandle.algorithm);
} catch (IOException | NoSuchAlgorithmException e) {
Slog.e(TAG, "Error computing the digest", e);
} finally {
Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
}
synchronized (mSessionLock) {
if (actualDigest != null && Arrays.equals(actualDigest, mBlobHandle.digest)) {
mState = STATE_VERIFIED_VALID;
// Commit callback will be sent once the data is persisted.
} else {
if (LOGV) {
Slog.v(TAG, "Digest of the data didn't match the given BlobHandle.digest");
}
mState = STATE_VERIFIED_INVALID;
sendCommitCallbackResult(COMMIT_RESULT_ERROR);
}
@@ -447,6 +457,16 @@ class BlobStoreSession extends IBlobStoreSession.Stub {
}
}
@Override
public String toString() {
return "BlobStoreSession {"
+ "id:" + mSessionId
+ ",handle:" + mBlobHandle
+ ",uid:" + mOwnerUid
+ ",pkg:" + mOwnerPackageName
+ "}";
}
private void assertCallerIsOwner() {
final int callingUid = Binder.getCallingUid();
if (callingUid != mOwnerUid) {

View File

@@ -5328,6 +5328,10 @@
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
<service android:name="com.android.server.blob.BlobStoreIdleJobService"
android:permission="android.permission.BIND_JOB_SERVICE">
</service>
<service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader">
<intent-filter>
<action android:name="android.intent.action.LOAD_DATA" />

View File

@@ -22,6 +22,7 @@ android_test {
"services.net",
"service-jobscheduler",
"service-permission",
"service-blobstore",
"androidx.test.runner",
"mockito-target-extended-minus-junit4",
"platform-test-annotations",

View File

@@ -0,0 +1,342 @@
/*
* Copyright 2020 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.blob;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.server.blob.BlobStoreConfig.SESSION_EXPIRY_TIMEOUT_MILLIS;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.app.blob.BlobHandle;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.util.ArrayMap;
import android.util.LongSparseArray;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.blob.BlobStoreManagerService.Injector;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
import java.io.File;
@RunWith(AndroidJUnit4.class)
@SmallTest
@Presubmit
public class BlobStoreManagerServiceTest {
private Context mContext;
private Handler mHandler;
private BlobStoreManagerService mService;
private MockitoSession mMockitoSession;
@Mock
private File mBlobsDir;
private LongSparseArray<BlobStoreSession> mUserSessions;
private ArrayMap<BlobHandle, BlobMetadata> mUserBlobs;
private static final String TEST_PKG1 = "com.example1";
private static final String TEST_PKG2 = "com.example2";
private static final String TEST_PKG3 = "com.example3";
private static final int TEST_UID1 = 10001;
private static final int TEST_UID2 = 10002;
private static final int TEST_UID3 = 10003;
@Before
public void setUp() {
// Share classloader to allow package private access.
System.setProperty("dexmaker.share_classloader", "true");
mMockitoSession = mockitoSession()
.initMocks(this)
.strictness(Strictness.LENIENT)
.mockStatic(BlobStoreConfig.class)
.startMocking();
doReturn(mBlobsDir).when(() -> BlobStoreConfig.getBlobsDir());
doReturn(true).when(mBlobsDir).exists();
doReturn(new File[0]).when(mBlobsDir).listFiles();
mContext = InstrumentationRegistry.getTargetContext();
mHandler = new TestHandler(Looper.getMainLooper());
mService = new BlobStoreManagerService(mContext, new TestInjector());
mUserSessions = new LongSparseArray<>();
mUserBlobs = new ArrayMap<>();
mService.addUserSessionsForTest(mUserSessions, UserHandle.myUserId());
mService.addUserBlobsForTest(mUserBlobs, UserHandle.myUserId());
}
@After
public void tearDown() {
if (mMockitoSession != null) {
mMockitoSession.finishMocking();
}
}
@Test
public void testHandlePackageRemoved() throws Exception {
// Setup sessions
final File sessionFile1 = mock(File.class);
final long sessionId1 = 11;
final BlobStoreSession session1 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
sessionId1, sessionFile1);
mUserSessions.append(sessionId1, session1);
final File sessionFile2 = mock(File.class);
final long sessionId2 = 25;
final BlobStoreSession session2 = createBlobStoreSessionMock(TEST_PKG2, TEST_UID2,
sessionId2, sessionFile2);
mUserSessions.append(sessionId2, session2);
final File sessionFile3 = mock(File.class);
final long sessionId3 = 37;
final BlobStoreSession session3 = createBlobStoreSessionMock(TEST_PKG3, TEST_UID3,
sessionId3, sessionFile3);
mUserSessions.append(sessionId3, session3);
final File sessionFile4 = mock(File.class);
final long sessionId4 = 48;
final BlobStoreSession session4 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
sessionId4, sessionFile4);
mUserSessions.append(sessionId4, session4);
// Setup blobs
final long blobId1 = 978;
final File blobFile1 = mock(File.class);
final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
"label1", System.currentTimeMillis(), "tag1");
final BlobMetadata blobMetadata1 = createBlobMetadataMock(blobId1, blobFile1, true);
mUserBlobs.put(blobHandle1, blobMetadata1);
final long blobId2 = 347;
final File blobFile2 = mock(File.class);
final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest2".getBytes(),
"label2", System.currentTimeMillis(), "tag2");
final BlobMetadata blobMetadata2 = createBlobMetadataMock(blobId2, blobFile2, false);
mUserBlobs.put(blobHandle2, blobMetadata2);
mService.addKnownIdsForTest(sessionId1, sessionId2, sessionId3, sessionId4,
blobId1, blobId2);
// Invoke test method
mService.handlePackageRemoved(TEST_PKG1, TEST_UID1);
// Verify sessions are removed
verify(sessionFile1).delete();
verify(sessionFile2, never()).delete();
verify(sessionFile3, never()).delete();
verify(sessionFile4).delete();
assertThat(mUserSessions.size()).isEqualTo(2);
assertThat(mUserSessions.get(sessionId1)).isNull();
assertThat(mUserSessions.get(sessionId2)).isNotNull();
assertThat(mUserSessions.get(sessionId3)).isNotNull();
assertThat(mUserSessions.get(sessionId4)).isNull();
// Verify blobs are removed
verify(blobMetadata1).removeCommitter(TEST_PKG1, TEST_UID1);
verify(blobMetadata1).removeLeasee(TEST_PKG1, TEST_UID1);
verify(blobMetadata2).removeCommitter(TEST_PKG1, TEST_UID1);
verify(blobMetadata2).removeLeasee(TEST_PKG1, TEST_UID1);
verify(blobFile1, never()).delete();
verify(blobFile2).delete();
assertThat(mUserBlobs.size()).isEqualTo(1);
assertThat(mUserBlobs.get(blobHandle1)).isNotNull();
assertThat(mUserBlobs.get(blobHandle2)).isNull();
assertThat(mService.getKnownIdsForTest()).containsExactly(
sessionId2, sessionId3, blobId1);
}
@Test
public void testHandleIdleMaintenance_deleteUnknownBlobs() throws Exception {
// Setup blob files
final long testId1 = 286;
final File file1 = mock(File.class);
doReturn(String.valueOf(testId1)).when(file1).getName();
final long testId2 = 349;
final File file2 = mock(File.class);
doReturn(String.valueOf(testId2)).when(file2).getName();
final long testId3 = 7355;
final File file3 = mock(File.class);
doReturn(String.valueOf(testId3)).when(file3).getName();
doReturn(new File[] {file1, file2, file3}).when(mBlobsDir).listFiles();
mService.addKnownIdsForTest(testId1, testId3);
// Invoke test method
mService.handleIdleMaintenanceLocked();
// Verify unknown blobs are delete
verify(file1, never()).delete();
verify(file2).delete();
verify(file3, never()).delete();
}
@Test
public void testHandleIdleMaintenance_deleteStaleSessions() throws Exception {
// Setup sessions
final File sessionFile1 = mock(File.class);
doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS + 1000)
.when(sessionFile1).lastModified();
final long sessionId1 = 342;
final BlobHandle blobHandle1 = mock(BlobHandle.class);
doReturn(System.currentTimeMillis() - 1000).when(blobHandle1).getExpiryTimeMillis();
final BlobStoreSession session1 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
sessionId1, sessionFile1, blobHandle1);
mUserSessions.append(sessionId1, session1);
final File sessionFile2 = mock(File.class);
doReturn(System.currentTimeMillis() - 20000)
.when(sessionFile2).lastModified();
final long sessionId2 = 4597;
final BlobHandle blobHandle2 = mock(BlobHandle.class);
doReturn(System.currentTimeMillis() + 20000).when(blobHandle2).getExpiryTimeMillis();
final BlobStoreSession session2 = createBlobStoreSessionMock(TEST_PKG2, TEST_UID2,
sessionId2, sessionFile2, blobHandle2);
mUserSessions.append(sessionId2, session2);
final File sessionFile3 = mock(File.class);
doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS - 2000)
.when(sessionFile3).lastModified();
final long sessionId3 = 9484;
final BlobHandle blobHandle3 = mock(BlobHandle.class);
doReturn(System.currentTimeMillis() + 30000).when(blobHandle3).getExpiryTimeMillis();
final BlobStoreSession session3 = createBlobStoreSessionMock(TEST_PKG3, TEST_UID3,
sessionId3, sessionFile3, blobHandle3);
mUserSessions.append(sessionId3, session3);
mService.addKnownIdsForTest(sessionId1, sessionId2, sessionId3);
// Invoke test method
mService.handleIdleMaintenanceLocked();
// Verify stale sessions are removed
verify(sessionFile1).delete();
verify(sessionFile2, never()).delete();
verify(sessionFile3).delete();
assertThat(mUserSessions.size()).isEqualTo(1);
assertThat(mUserSessions.get(sessionId2)).isNotNull();
assertThat(mService.getKnownIdsForTest()).containsExactly(sessionId2);
}
@Test
public void testHandleIdleMaintenance_deleteStaleBlobs() throws Exception {
// Setup blobs
final long blobId1 = 3489;
final File blobFile1 = mock(File.class);
final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
"label1", System.currentTimeMillis() - 2000, "tag1");
final BlobMetadata blobMetadata1 = createBlobMetadataMock(blobId1, blobFile1, true);
mUserBlobs.put(blobHandle1, blobMetadata1);
final long blobId2 = 78974;
final File blobFile2 = mock(File.class);
final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest2".getBytes(),
"label2", System.currentTimeMillis() + 30000, "tag2");
final BlobMetadata blobMetadata2 = createBlobMetadataMock(blobId2, blobFile2, true);
mUserBlobs.put(blobHandle2, blobMetadata2);
final long blobId3 = 97;
final File blobFile3 = mock(File.class);
final BlobHandle blobHandle3 = BlobHandle.createWithSha256("digest3".getBytes(),
"label3", System.currentTimeMillis() + 4400000, "tag3");
final BlobMetadata blobMetadata3 = createBlobMetadataMock(blobId3, blobFile3, false);
mUserBlobs.put(blobHandle3, blobMetadata3);
mService.addKnownIdsForTest(blobId1, blobId2, blobId3);
// Invoke test method
mService.handleIdleMaintenanceLocked();
// Verify stale blobs are removed
verify(blobFile1).delete();
verify(blobFile2, never()).delete();
verify(blobFile3).delete();
assertThat(mUserBlobs.size()).isEqualTo(1);
assertThat(mUserBlobs.get(blobHandle2)).isNotNull();
assertThat(mService.getKnownIdsForTest()).containsExactly(blobId2);
}
private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid,
long sessionId, File sessionFile) {
return createBlobStoreSessionMock(ownerPackageName, ownerUid, sessionId, sessionFile,
mock(BlobHandle.class));
}
private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid,
long sessionId, File sessionFile, BlobHandle blobHandle) {
final BlobStoreSession session = mock(BlobStoreSession.class);
doReturn(ownerPackageName).when(session).getOwnerPackageName();
doReturn(ownerUid).when(session).getOwnerUid();
doReturn(sessionId).when(session).getSessionId();
doReturn(sessionFile).when(session).getSessionFile();
doReturn(blobHandle).when(session).getBlobHandle();
return session;
}
private BlobMetadata createBlobMetadataMock(long blobId, File blobFile, boolean hasLeases) {
final BlobMetadata blobMetadata = mock(BlobMetadata.class);
doReturn(blobId).when(blobMetadata).getBlobId();
doReturn(blobFile).when(blobMetadata).getBlobFile();
doReturn(hasLeases).when(blobMetadata).hasLeases();
return blobMetadata;
}
private class TestHandler extends Handler {
TestHandler(Looper looper) {
super(looper);
}
@Override
public void dispatchMessage(Message msg) {
// Ignore all messages
}
}
private class TestInjector extends Injector {
@Override
public Handler initializeMessageHandler() {
return mHandler;
}
}
}

View File

@@ -46,7 +46,6 @@ android_test {
"service-appsearch",
"service-jobscheduler",
"service-permission",
"service-blobstore",
// TODO: remove once Android migrates to JUnit 4.12,
// which provides assertThrows
"testng",

View File

@@ -1,189 +0,0 @@
/*
* Copyright 2020 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.blob;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.blob.BlobHandle;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.util.ArrayMap;
import android.util.LongSparseArray;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.blob.BlobStoreManagerService.Injector;
import com.android.server.blob.BlobStoreManagerService.SessionStateChangeListener;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
@RunWith(AndroidJUnit4.class)
@SmallTest
@Presubmit
public class BlobStoreManagerServiceTest {
private Context mContext;
private Handler mHandler;
private BlobStoreManagerService mService;
private LongSparseArray<BlobStoreSession> mUserSessions;
private ArrayMap<BlobHandle, BlobMetadata> mUserBlobs;
private SessionStateChangeListener mStateChangeListener;
private static final String TEST_PKG1 = "com.example1";
private static final String TEST_PKG2 = "com.example2";
private static final String TEST_PKG3 = "com.example3";
private static final int TEST_UID1 = 10001;
private static final int TEST_UID2 = 10002;
private static final int TEST_UID3 = 10003;
@Before
public void setUp() {
// Share classloader to allow package private access.
System.setProperty("dexmaker.share_classloader", "true");
mContext = InstrumentationRegistry.getTargetContext();
mHandler = new TestHandler(Looper.getMainLooper());
mService = new BlobStoreManagerService(mContext, new TestInjector());
mUserSessions = new LongSparseArray<>();
mUserBlobs = new ArrayMap<>();
mService.addUserSessionsForTest(mUserSessions, UserHandle.myUserId());
mService.addUserBlobsForTest(mUserBlobs, UserHandle.myUserId());
mStateChangeListener = mService.new SessionStateChangeListener();
}
@Test
public void testHandlePackageRemoved() throws Exception {
// Setup sessions
final File sessionFile1 = mock(File.class);
final long sessionId1 = 11;
final BlobStoreSession session1 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
sessionId1, sessionFile1);
mUserSessions.append(sessionId1, session1);
final File sessionFile2 = mock(File.class);
final long sessionId2 = 25;
final BlobStoreSession session2 = createBlobStoreSessionMock(TEST_PKG2, TEST_UID2,
sessionId2, sessionFile2);
mUserSessions.append(sessionId2, session2);
final File sessionFile3 = mock(File.class);
final long sessionId3 = 37;
final BlobStoreSession session3 = createBlobStoreSessionMock(TEST_PKG3, TEST_UID3,
sessionId3, sessionFile3);
mUserSessions.append(sessionId3, session3);
final File sessionFile4 = mock(File.class);
final long sessionId4 = 48;
final BlobStoreSession session4 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
sessionId4, sessionFile4);
mUserSessions.append(sessionId4, session4);
// Setup blobs
final File blobFile1 = mock(File.class);
final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
"label1", System.currentTimeMillis(), "tag1");
final BlobMetadata blobMetadata1 = createBlobMetadataMock(blobFile1, true);
mUserBlobs.put(blobHandle1, blobMetadata1);
final File blobFile2 = mock(File.class);
final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest2".getBytes(),
"label2", System.currentTimeMillis(), "tag2");
final BlobMetadata blobMetadata2 = createBlobMetadataMock(blobFile2, false);
mUserBlobs.put(blobHandle2, blobMetadata2);
// Invoke test method
mService.handlePackageRemoved(TEST_PKG1, TEST_UID1);
// Verify sessions are removed
verify(sessionFile1).delete();
verify(sessionFile2, never()).delete();
verify(sessionFile3, never()).delete();
verify(sessionFile4).delete();
assertThat(mUserSessions.size()).isEqualTo(2);
assertThat(mUserSessions.get(sessionId1)).isNull();
assertThat(mUserSessions.get(sessionId2)).isNotNull();
assertThat(mUserSessions.get(sessionId3)).isNotNull();
assertThat(mUserSessions.get(sessionId4)).isNull();
// Verify blobs are removed
verify(blobMetadata1).removeCommitter(TEST_PKG1, TEST_UID1);
verify(blobMetadata1).removeLeasee(TEST_PKG1, TEST_UID1);
verify(blobMetadata2).removeCommitter(TEST_PKG1, TEST_UID1);
verify(blobMetadata2).removeLeasee(TEST_PKG1, TEST_UID1);
verify(blobFile1, never()).delete();
verify(blobFile2).delete();
assertThat(mUserBlobs.size()).isEqualTo(1);
assertThat(mUserBlobs.get(blobHandle1)).isNotNull();
assertThat(mUserBlobs.get(blobHandle2)).isNull();
}
private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid,
long sessionId, File sessionFile) {
final BlobStoreSession session = mock(BlobStoreSession.class);
when(session.getOwnerPackageName()).thenReturn(ownerPackageName);
when(session.getOwnerUid()).thenReturn(ownerUid);
when(session.getSessionId()).thenReturn(sessionId);
when(session.getSessionFile()).thenReturn(sessionFile);
return session;
}
private BlobMetadata createBlobMetadataMock(File blobFile, boolean hasLeases) {
final BlobMetadata blobMetadata = mock(BlobMetadata.class);
when(blobMetadata.getBlobFile()).thenReturn(blobFile);
when(blobMetadata.hasLeases()).thenReturn(hasLeases);
return blobMetadata;
}
private class TestHandler extends Handler {
TestHandler(Looper looper) {
super(looper);
}
@Override
public void dispatchMessage(Message msg) {
// Ignore all messages
}
}
private class TestInjector extends Injector {
@Override
public Handler initializeMessageHandler() {
return mHandler;
}
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (C) 2020 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.
java_library {
name: "BlobStoreTestUtils",
srcs: ["src/**/*.java"],
static_libs: ["truth-prebuilt"],
platform_apis: true
}

View File

@@ -0,0 +1,227 @@
/*
* Copyright 2020 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.utils.blob;
import static com.google.common.truth.Truth.assertThat;
import android.app.blob.BlobHandle;
import android.app.blob.BlobStoreManager;
import android.content.Context;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class DummyBlobData {
private static final long DEFAULT_SIZE_BYTES = 10 * 1024L * 1024L;
private static final int BUFFER_SIZE_BYTES = 16 * 1024;
private final Context mContext;
private final Random mRandom;
private final File mFile;
private final long mFileSize;
private final String mLabel;
byte[] mFileDigest;
long mExpiryTimeMs;
public DummyBlobData(Context context) {
this(context, new Random(0), "blob_" + System.nanoTime());
}
public DummyBlobData(Context context, long fileSize) {
this(context, fileSize, new Random(0), "blob_" + System.nanoTime(), "Test label");
}
public DummyBlobData(Context context, Random random, String fileName) {
this(context, DEFAULT_SIZE_BYTES, random, fileName, "Test label");
}
public DummyBlobData(Context context, Random random, String fileName, String label) {
this(context, DEFAULT_SIZE_BYTES, random, fileName, label);
}
public DummyBlobData(Context context, long fileSize, Random random, String fileName,
String label) {
mContext = context;
mRandom = random;
mFile = new File(mContext.getFilesDir(), fileName);
mFileSize = fileSize;
mLabel = label;
}
public void prepare() throws Exception {
try (RandomAccessFile file = new RandomAccessFile(mFile, "rw")) {
writeRandomData(file, mFileSize);
}
mFileDigest = FileUtils.digest(mFile, "SHA-256");
mExpiryTimeMs = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
}
public BlobHandle getBlobHandle() throws Exception {
return BlobHandle.createWithSha256(createSha256Digest(mFile), mLabel,
mExpiryTimeMs, "test_tag");
}
public long getFileSize() throws Exception {
return mFileSize;
}
public long getExpiryTimeMillis() {
return mExpiryTimeMs;
}
public void delete() {
mFile.delete();
}
public void writeToSession(BlobStoreManager.Session session) throws Exception {
writeToSession(session, 0, mFileSize);
}
public void writeToSession(BlobStoreManager.Session session,
long offsetBytes, long lengthBytes) throws Exception {
try (FileInputStream in = new FileInputStream(mFile)) {
in.getChannel().position(offsetBytes);
try (FileOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(
session.openWrite(offsetBytes, lengthBytes))) {
copy(in, out, lengthBytes);
}
}
}
public void writeToFd(FileDescriptor fd, long offsetBytes, long lengthBytes) throws Exception {
try (FileInputStream in = new FileInputStream(mFile)) {
in.getChannel().position(offsetBytes);
try (FileOutputStream out = new FileOutputStream(fd)) {
copy(in, out, lengthBytes);
}
}
}
private void copy(InputStream in, OutputStream out, long lengthBytes) throws Exception {
final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
long bytesWrittern = 0;
while (bytesWrittern < lengthBytes) {
final int toWrite = (bytesWrittern + buffer.length <= lengthBytes)
? buffer.length : (int) (lengthBytes - bytesWrittern);
in.read(buffer, 0, toWrite);
out.write(buffer, 0, toWrite);
bytesWrittern += toWrite;
}
}
public void readFromSessionAndVerifyBytes(BlobStoreManager.Session session,
long offsetBytes, int lengthBytes) throws Exception {
final byte[] expectedBytes = new byte[lengthBytes];
try (FileInputStream in = new FileInputStream(mFile)) {
read(in, expectedBytes, offsetBytes, lengthBytes);
}
final byte[] actualBytes = new byte[lengthBytes];
try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
session.openWrite(0L, 0L))) {
read(in, actualBytes, offsetBytes, lengthBytes);
}
assertThat(actualBytes).isEqualTo(expectedBytes);
}
private void read(FileInputStream in, byte[] buffer,
long offsetBytes, int lengthBytes) throws Exception {
in.getChannel().position(offsetBytes);
in.read(buffer, 0, lengthBytes);
}
public void readFromSessionAndVerifyDigest(BlobStoreManager.Session session)
throws Exception {
readFromSessionAndVerifyDigest(session, 0, mFile.length());
}
public void readFromSessionAndVerifyDigest(BlobStoreManager.Session session,
long offsetBytes, long lengthBytes) throws Exception {
final byte[] actualDigest;
try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
session.openWrite(0L, 0L))) {
actualDigest = createSha256Digest(in, offsetBytes, lengthBytes);
}
assertThat(actualDigest).isEqualTo(mFileDigest);
}
public void verifyBlob(ParcelFileDescriptor pfd) throws Exception {
final byte[] actualDigest;
try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
actualDigest = FileUtils.digest(in, "SHA-256");
}
assertThat(actualDigest).isEqualTo(mFileDigest);
}
private byte[] createSha256Digest(FileInputStream in, long offsetBytes, long lengthBytes)
throws Exception {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
in.getChannel().position(offsetBytes);
final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
long bytesRead = 0;
while (bytesRead < lengthBytes) {
int toRead = (bytesRead + buffer.length <= lengthBytes)
? buffer.length : (int) (lengthBytes - bytesRead);
toRead = in.read(buffer, 0, toRead);
digest.update(buffer, 0, toRead);
bytesRead += toRead;
}
return digest.digest();
}
private byte[] createSha256Digest(File file) throws Exception {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (BufferedInputStream in = new BufferedInputStream(
Files.newInputStream(file.toPath()))) {
final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
int bytesRead;
while ((bytesRead = in.read(buffer)) > 0) {
digest.update(buffer, 0, bytesRead);
}
}
return digest.digest();
}
private void writeRandomData(RandomAccessFile file, long fileSize)
throws Exception {
long bytesWritten = 0;
final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
while (bytesWritten < fileSize) {
mRandom.nextBytes(buffer);
final int toWrite = (bytesWritten + buffer.length <= fileSize)
? buffer.length : (int) (fileSize - bytesWritten);
file.seek(bytesWritten);
file.write(buffer, 0, toWrite);
bytesWritten += toWrite;
}
}
}