blob: d08930b007ffb3c76f9d3ca51e6e928fe7f3f2b1 [file] [log] [blame]
/*
* 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.annotation.UnsupportedAppUsage;
import android.os.ProxyFileDescriptorCallback;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
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.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) {
Preconditions.checkNotNull(callback);
Preconditions.checkNotNull(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(entry.getThreadId());
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
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
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(entry.getThreadId());
}
} 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 = Preconditions.checkNotNull(callback);
this.handler = Preconditions.checkNotNull(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 Thread ID and byte buffer.
*/
private static class BytesMap {
final Map<Long, BytesMapEntry> mEntries = new HashMap<>();
byte[] startUsing(long threadId) {
BytesMapEntry entry = mEntries.get(threadId);
if (entry == null) {
entry = new BytesMapEntry();
mEntries.put(threadId, entry);
}
entry.counter++;
return entry.bytes;
}
void stopUsing(long threadId) {
final BytesMapEntry entry = mEntries.get(threadId);
Preconditions.checkNotNull(entry);
entry.counter--;
if (entry.counter <= 0) {
mEntries.remove(threadId);
}
}
void clear() {
mEntries.clear();
}
}
private static class Args {
long unique;
long inode;
long offset;
int size;
byte[] data;
CallbackEntry entry;
}
}