/* * 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.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 mCallbackMap = new SparseArray<>(); @GuardedBy("mLock") private final BytesMap mBytesMap = new BytesMap(); @GuardedBy("mLock") private final LinkedList 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 = 256 * 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") 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") 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; } 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; } private void recycleLocked(Args args) { if (mArgsPool.size() < ARGS_POOL_SIZE) { mArgsPool.add(args); } } 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 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; } }