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:
committed by
Android (Google) Code Review
commit
36faba3e7b
28
apct-tests/perftests/blobstore/Android.bp
Normal file
28
apct-tests/perftests/blobstore/Android.bp
Normal 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",
|
||||
}
|
||||
27
apct-tests/perftests/blobstore/AndroidManifest.xml
Normal file
27
apct-tests/perftests/blobstore/AndroidManifest.xml
Normal 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>
|
||||
28
apct-tests/perftests/blobstore/AndroidTest.xml
Normal file
28
apct-tests/perftests/blobstore/AndroidTest.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"name": "CtsBlobStoreTestCases"
|
||||
},
|
||||
{
|
||||
"name": "FrameworksServicesTests",
|
||||
"name": "FrameworksMockingServicesTests",
|
||||
"options": [
|
||||
{
|
||||
"include-filter": "com.android.server.blob"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
tests/BlobStoreTestUtils/Android.bp
Normal file
20
tests/BlobStoreTestUtils/Android.bp
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user