396 lines
13 KiB
Java
396 lines
13 KiB
Java
/*
|
|
* Copyright (C) 2016 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.internal.os;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.os.Build;
|
|
import android.os.Handler;
|
|
import android.os.Message;
|
|
import android.os.ParcelFileDescriptor;
|
|
import android.os.ProxyFileDescriptorCallback;
|
|
import android.system.ErrnoException;
|
|
import android.system.OsConstants;
|
|
import android.util.Log;
|
|
import android.util.SparseArray;
|
|
|
|
import com.android.internal.annotations.GuardedBy;
|
|
import com.android.internal.util.Preconditions;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.LinkedList;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.ThreadFactory;
|
|
|
|
public class FuseAppLoop implements Handler.Callback {
|
|
private static final String TAG = "FuseAppLoop";
|
|
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
|
public static final int ROOT_INODE = 1;
|
|
private static final int MIN_INODE = 2;
|
|
private static final ThreadFactory sDefaultThreadFactory = new ThreadFactory() {
|
|
@Override
|
|
public Thread newThread(Runnable r) {
|
|
return new Thread(r, TAG);
|
|
}
|
|
};
|
|
private static final int FUSE_OK = 0;
|
|
private static final int ARGS_POOL_SIZE = 50;
|
|
|
|
private final Object mLock = new Object();
|
|
private final int mMountPointId;
|
|
private final Thread mThread;
|
|
|
|
@GuardedBy("mLock")
|
|
private final SparseArray<CallbackEntry> mCallbackMap = new SparseArray<>();
|
|
|
|
@GuardedBy("mLock")
|
|
private final BytesMap mBytesMap = new BytesMap();
|
|
|
|
@GuardedBy("mLock")
|
|
private final LinkedList<Args> mArgsPool = new LinkedList<>();
|
|
|
|
/**
|
|
* Sequential number can be used as file name and inode in AppFuse.
|
|
* 0 is regarded as an error, 1 is mount point. So we start the number from 2.
|
|
*/
|
|
@GuardedBy("mLock")
|
|
private int mNextInode = MIN_INODE;
|
|
|
|
@GuardedBy("mLock")
|
|
private long mInstance;
|
|
|
|
public FuseAppLoop(
|
|
int mountPointId, @NonNull ParcelFileDescriptor fd, @Nullable ThreadFactory factory) {
|
|
mMountPointId = mountPointId;
|
|
if (factory == null) {
|
|
factory = sDefaultThreadFactory;
|
|
}
|
|
mInstance = native_new(fd.detachFd());
|
|
mThread = factory.newThread(() -> {
|
|
native_start(mInstance);
|
|
synchronized (mLock) {
|
|
native_delete(mInstance);
|
|
mInstance = 0;
|
|
mBytesMap.clear();
|
|
}
|
|
});
|
|
mThread.start();
|
|
}
|
|
|
|
public int registerCallback(@NonNull ProxyFileDescriptorCallback callback,
|
|
@NonNull Handler handler) throws FuseUnavailableMountException {
|
|
synchronized (mLock) {
|
|
Objects.requireNonNull(callback);
|
|
Objects.requireNonNull(handler);
|
|
Preconditions.checkState(
|
|
mCallbackMap.size() < Integer.MAX_VALUE - MIN_INODE, "Too many opened files.");
|
|
Preconditions.checkArgument(
|
|
Thread.currentThread().getId() != handler.getLooper().getThread().getId(),
|
|
"Handler must be different from the current thread");
|
|
if (mInstance == 0) {
|
|
throw new FuseUnavailableMountException(mMountPointId);
|
|
}
|
|
int id;
|
|
while (true) {
|
|
id = mNextInode;
|
|
mNextInode++;
|
|
if (mNextInode < 0) {
|
|
mNextInode = MIN_INODE;
|
|
}
|
|
if (mCallbackMap.get(id) == null) {
|
|
break;
|
|
}
|
|
}
|
|
mCallbackMap.put(id, new CallbackEntry(
|
|
callback, new Handler(handler.getLooper(), this)));
|
|
return id;
|
|
}
|
|
}
|
|
|
|
public void unregisterCallback(int id) {
|
|
synchronized (mLock) {
|
|
mCallbackMap.remove(id);
|
|
}
|
|
}
|
|
|
|
public int getMountPointId() {
|
|
return mMountPointId;
|
|
}
|
|
|
|
// Defined in fuse.h
|
|
private static final int FUSE_LOOKUP = 1;
|
|
private static final int FUSE_GETATTR = 3;
|
|
private static final int FUSE_OPEN = 14;
|
|
private static final int FUSE_READ = 15;
|
|
private static final int FUSE_WRITE = 16;
|
|
private static final int FUSE_RELEASE = 18;
|
|
private static final int FUSE_FSYNC = 20;
|
|
|
|
// Defined in FuseBuffer.h
|
|
private static final int FUSE_MAX_WRITE = 128 * 1024;
|
|
|
|
@Override
|
|
public boolean handleMessage(Message msg) {
|
|
final Args args = (Args) msg.obj;
|
|
final CallbackEntry entry = args.entry;
|
|
final long inode = args.inode;
|
|
final long unique = args.unique;
|
|
final int size = args.size;
|
|
final long offset = args.offset;
|
|
final byte[] data = args.data;
|
|
|
|
try {
|
|
switch (msg.what) {
|
|
case FUSE_LOOKUP: {
|
|
final long fileSize = entry.callback.onGetSize();
|
|
synchronized (mLock) {
|
|
if (mInstance != 0) {
|
|
native_replyLookup(mInstance, unique, inode, fileSize);
|
|
}
|
|
recycleLocked(args);
|
|
}
|
|
break;
|
|
}
|
|
case FUSE_GETATTR: {
|
|
final long fileSize = entry.callback.onGetSize();
|
|
synchronized (mLock) {
|
|
if (mInstance != 0) {
|
|
native_replyGetAttr(mInstance, unique, inode, fileSize);
|
|
}
|
|
recycleLocked(args);
|
|
}
|
|
break;
|
|
}
|
|
case FUSE_READ:
|
|
final int readSize = entry.callback.onRead(
|
|
offset, size, data);
|
|
synchronized (mLock) {
|
|
if (mInstance != 0) {
|
|
native_replyRead(mInstance, unique, readSize, data);
|
|
}
|
|
recycleLocked(args);
|
|
}
|
|
break;
|
|
case FUSE_WRITE:
|
|
final int writeSize = entry.callback.onWrite(offset, size, data);
|
|
synchronized (mLock) {
|
|
if (mInstance != 0) {
|
|
native_replyWrite(mInstance, unique, writeSize);
|
|
}
|
|
recycleLocked(args);
|
|
}
|
|
break;
|
|
case FUSE_FSYNC:
|
|
entry.callback.onFsync();
|
|
synchronized (mLock) {
|
|
if (mInstance != 0) {
|
|
native_replySimple(mInstance, unique, FUSE_OK);
|
|
}
|
|
recycleLocked(args);
|
|
}
|
|
break;
|
|
case FUSE_RELEASE:
|
|
entry.callback.onRelease();
|
|
synchronized (mLock) {
|
|
if (mInstance != 0) {
|
|
native_replySimple(mInstance, unique, FUSE_OK);
|
|
}
|
|
mBytesMap.stopUsing(inode);
|
|
recycleLocked(args);
|
|
}
|
|
break;
|
|
default:
|
|
throw new IllegalArgumentException("Unknown FUSE command: " + msg.what);
|
|
}
|
|
} catch (Exception error) {
|
|
synchronized (mLock) {
|
|
Log.e(TAG, "", error);
|
|
replySimpleLocked(unique, getError(error));
|
|
recycleLocked(args);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Called by JNI.
|
|
@SuppressWarnings("unused")
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private void onCommand(int command, long unique, long inode, long offset, int size,
|
|
byte[] data) {
|
|
synchronized (mLock) {
|
|
try {
|
|
final Args args;
|
|
if (mArgsPool.size() == 0) {
|
|
args = new Args();
|
|
} else {
|
|
args = mArgsPool.pop();
|
|
}
|
|
args.unique = unique;
|
|
args.inode = inode;
|
|
args.offset = offset;
|
|
args.size = size;
|
|
args.data = data;
|
|
args.entry = getCallbackEntryOrThrowLocked(inode);
|
|
if (!args.entry.handler.sendMessage(
|
|
Message.obtain(args.entry.handler, command, 0, 0, args))) {
|
|
throw new ErrnoException("onCommand", OsConstants.EBADF);
|
|
}
|
|
} catch (Exception error) {
|
|
replySimpleLocked(unique, getError(error));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Called by JNI.
|
|
@SuppressWarnings("unused")
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private byte[] onOpen(long unique, long inode) {
|
|
synchronized (mLock) {
|
|
try {
|
|
final CallbackEntry entry = getCallbackEntryOrThrowLocked(inode);
|
|
if (entry.opened) {
|
|
throw new ErrnoException("onOpen", OsConstants.EMFILE);
|
|
}
|
|
if (mInstance != 0) {
|
|
native_replyOpen(mInstance, unique, /* fh */ inode);
|
|
entry.opened = true;
|
|
return mBytesMap.startUsing(inode);
|
|
}
|
|
} catch (ErrnoException error) {
|
|
replySimpleLocked(unique, getError(error));
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static int getError(@NonNull Exception error) {
|
|
if (error instanceof ErrnoException) {
|
|
final int errno = ((ErrnoException) error).errno;
|
|
if (errno != OsConstants.ENOSYS) {
|
|
return -errno;
|
|
}
|
|
}
|
|
return -OsConstants.EBADF;
|
|
}
|
|
|
|
@GuardedBy("mLock")
|
|
private CallbackEntry getCallbackEntryOrThrowLocked(long inode) throws ErrnoException {
|
|
final CallbackEntry entry = mCallbackMap.get(checkInode(inode));
|
|
if (entry == null) {
|
|
throw new ErrnoException("getCallbackEntryOrThrowLocked", OsConstants.ENOENT);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
@GuardedBy("mLock")
|
|
private void recycleLocked(Args args) {
|
|
if (mArgsPool.size() < ARGS_POOL_SIZE) {
|
|
mArgsPool.add(args);
|
|
}
|
|
}
|
|
|
|
@GuardedBy("mLock")
|
|
private void replySimpleLocked(long unique, int result) {
|
|
if (mInstance != 0) {
|
|
native_replySimple(mInstance, unique, result);
|
|
}
|
|
}
|
|
|
|
native long native_new(int fd);
|
|
native void native_delete(long ptr);
|
|
native void native_start(long ptr);
|
|
|
|
native void native_replySimple(long ptr, long unique, int result);
|
|
native void native_replyOpen(long ptr, long unique, long fh);
|
|
native void native_replyLookup(long ptr, long unique, long inode, long size);
|
|
native void native_replyGetAttr(long ptr, long unique, long inode, long size);
|
|
native void native_replyWrite(long ptr, long unique, int size);
|
|
native void native_replyRead(long ptr, long unique, int size, byte[] bytes);
|
|
|
|
private static int checkInode(long inode) {
|
|
Preconditions.checkArgumentInRange(inode, MIN_INODE, Integer.MAX_VALUE, "checkInode");
|
|
return (int) inode;
|
|
}
|
|
|
|
public static class UnmountedException extends Exception {}
|
|
|
|
private static class CallbackEntry {
|
|
final ProxyFileDescriptorCallback callback;
|
|
final Handler handler;
|
|
boolean opened;
|
|
|
|
CallbackEntry(ProxyFileDescriptorCallback callback, Handler handler) {
|
|
this.callback = Objects.requireNonNull(callback);
|
|
this.handler = Objects.requireNonNull(handler);
|
|
}
|
|
|
|
long getThreadId() {
|
|
return handler.getLooper().getThread().getId();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Entry for bytes map.
|
|
*/
|
|
private static class BytesMapEntry {
|
|
int counter = 0;
|
|
byte[] bytes = new byte[FUSE_MAX_WRITE];
|
|
}
|
|
|
|
/**
|
|
* Map between inode and byte buffer.
|
|
*/
|
|
private static class BytesMap {
|
|
final Map<Long, BytesMapEntry> mEntries = new HashMap<>();
|
|
|
|
byte[] startUsing(long inode) {
|
|
BytesMapEntry entry = mEntries.get(inode);
|
|
if (entry == null) {
|
|
entry = new BytesMapEntry();
|
|
mEntries.put(inode, entry);
|
|
}
|
|
entry.counter++;
|
|
return entry.bytes;
|
|
}
|
|
|
|
void stopUsing(long inode) {
|
|
final BytesMapEntry entry = mEntries.get(inode);
|
|
Objects.requireNonNull(entry);
|
|
entry.counter--;
|
|
if (entry.counter <= 0) {
|
|
mEntries.remove(inode);
|
|
}
|
|
}
|
|
|
|
void clear() {
|
|
mEntries.clear();
|
|
}
|
|
}
|
|
|
|
private static class Args {
|
|
long unique;
|
|
long inode;
|
|
long offset;
|
|
int size;
|
|
byte[] data;
|
|
CallbackEntry entry;
|
|
}
|
|
}
|