1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.os;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.os.ProxyFileDescriptorCallback;
22import android.os.Handler;
23import android.os.Message;
24import android.os.ParcelFileDescriptor;
25import android.system.ErrnoException;
26import android.system.OsConstants;
27import android.util.Log;
28import android.util.SparseArray;
29import com.android.internal.annotations.GuardedBy;
30import com.android.internal.util.Preconditions;
31import java.util.HashMap;
32import java.util.LinkedList;
33import java.util.Map;
34import java.util.concurrent.ThreadFactory;
35
36public class FuseAppLoop implements Handler.Callback {
37    private static final String TAG = "FuseAppLoop";
38    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
39    public static final int ROOT_INODE = 1;
40    private static final int MIN_INODE = 2;
41    private static final ThreadFactory sDefaultThreadFactory = new ThreadFactory() {
42        @Override
43        public Thread newThread(Runnable r) {
44            return new Thread(r, TAG);
45        }
46    };
47    private static final int FUSE_OK = 0;
48    private static final int ARGS_POOL_SIZE = 50;
49
50    private final Object mLock = new Object();
51    private final int mMountPointId;
52    private final Thread mThread;
53
54    @GuardedBy("mLock")
55    private final SparseArray<CallbackEntry> mCallbackMap = new SparseArray<>();
56
57    @GuardedBy("mLock")
58    private final BytesMap mBytesMap = new BytesMap();
59
60    @GuardedBy("mLock")
61    private final LinkedList<Args> mArgsPool = new LinkedList<>();
62
63    /**
64     * Sequential number can be used as file name and inode in AppFuse.
65     * 0 is regarded as an error, 1 is mount point. So we start the number from 2.
66     */
67    @GuardedBy("mLock")
68    private int mNextInode = MIN_INODE;
69
70    @GuardedBy("mLock")
71    private long mInstance;
72
73    public FuseAppLoop(
74            int mountPointId, @NonNull ParcelFileDescriptor fd, @Nullable ThreadFactory factory) {
75        mMountPointId = mountPointId;
76        if (factory == null) {
77            factory = sDefaultThreadFactory;
78        }
79        mInstance = native_new(fd.detachFd());
80        mThread = factory.newThread(() -> {
81            native_start(mInstance);
82            synchronized (mLock) {
83                native_delete(mInstance);
84                mInstance = 0;
85                mBytesMap.clear();
86            }
87        });
88        mThread.start();
89    }
90
91    public int registerCallback(@NonNull ProxyFileDescriptorCallback callback,
92            @NonNull Handler handler) throws FuseUnavailableMountException {
93        synchronized (mLock) {
94            Preconditions.checkNotNull(callback);
95            Preconditions.checkNotNull(handler);
96            Preconditions.checkState(
97                    mCallbackMap.size() < Integer.MAX_VALUE - MIN_INODE, "Too many opened files.");
98            Preconditions.checkArgument(
99                    Thread.currentThread().getId() != handler.getLooper().getThread().getId(),
100                    "Handler must be different from the current thread");
101            if (mInstance == 0) {
102                throw new FuseUnavailableMountException(mMountPointId);
103            }
104            int id;
105            while (true) {
106                id = mNextInode;
107                mNextInode++;
108                if (mNextInode < 0) {
109                    mNextInode = MIN_INODE;
110                }
111                if (mCallbackMap.get(id) == null) {
112                    break;
113                }
114            }
115            mCallbackMap.put(id, new CallbackEntry(
116                    callback, new Handler(handler.getLooper(), this)));
117            return id;
118        }
119    }
120
121    public void unregisterCallback(int id) {
122        synchronized (mLock) {
123            mCallbackMap.remove(id);
124        }
125    }
126
127    public int getMountPointId() {
128        return mMountPointId;
129    }
130
131    // Defined in fuse.h
132    private static final int FUSE_LOOKUP = 1;
133    private static final int FUSE_GETATTR = 3;
134    private static final int FUSE_OPEN = 14;
135    private static final int FUSE_READ = 15;
136    private static final int FUSE_WRITE = 16;
137    private static final int FUSE_RELEASE = 18;
138    private static final int FUSE_FSYNC = 20;
139
140    // Defined in FuseBuffer.h
141    private static final int FUSE_MAX_WRITE = 256 * 1024;
142
143    @Override
144    public boolean handleMessage(Message msg) {
145        final Args args = (Args) msg.obj;
146        final CallbackEntry entry = args.entry;
147        final long inode = args.inode;
148        final long unique = args.unique;
149        final int size = args.size;
150        final long offset = args.offset;
151        final byte[] data = args.data;
152
153        try {
154            switch (msg.what) {
155                case FUSE_LOOKUP: {
156                    final long fileSize = entry.callback.onGetSize();
157                    synchronized (mLock) {
158                        if (mInstance != 0) {
159                            native_replyLookup(mInstance, unique, inode, fileSize);
160                        }
161                        recycleLocked(args);
162                    }
163                    break;
164                }
165                case FUSE_GETATTR: {
166                    final long fileSize = entry.callback.onGetSize();
167                    synchronized (mLock) {
168                        if (mInstance != 0) {
169                            native_replyGetAttr(mInstance, unique, inode, fileSize);
170                        }
171                        recycleLocked(args);
172                    }
173                    break;
174                }
175                case FUSE_READ:
176                    final int readSize = entry.callback.onRead(
177                            offset, size, data);
178                    synchronized (mLock) {
179                        if (mInstance != 0) {
180                            native_replyRead(mInstance, unique, readSize, data);
181                        }
182                        recycleLocked(args);
183                    }
184                    break;
185                case FUSE_WRITE:
186                    final int writeSize = entry.callback.onWrite(offset, size, data);
187                    synchronized (mLock) {
188                        if (mInstance != 0) {
189                            native_replyWrite(mInstance, unique, writeSize);
190                        }
191                        recycleLocked(args);
192                    }
193                    break;
194                case FUSE_FSYNC:
195                    entry.callback.onFsync();
196                    synchronized (mLock) {
197                        if (mInstance != 0) {
198                            native_replySimple(mInstance, unique, FUSE_OK);
199                        }
200                        recycleLocked(args);
201                    }
202                    break;
203                case FUSE_RELEASE:
204                    entry.callback.onRelease();
205                    synchronized (mLock) {
206                        if (mInstance != 0) {
207                            native_replySimple(mInstance, unique, FUSE_OK);
208                        }
209                        mBytesMap.stopUsing(entry.getThreadId());
210                        recycleLocked(args);
211                    }
212                    break;
213                default:
214                    throw new IllegalArgumentException("Unknown FUSE command: " + msg.what);
215            }
216        } catch (Exception error) {
217            synchronized (mLock) {
218                Log.e(TAG, "", error);
219                replySimpleLocked(unique, getError(error));
220                recycleLocked(args);
221            }
222        }
223
224        return true;
225    }
226
227    // Called by JNI.
228    @SuppressWarnings("unused")
229    private void onCommand(int command, long unique, long inode, long offset, int size,
230            byte[] data) {
231        synchronized (mLock) {
232            try {
233                final Args args;
234                if (mArgsPool.size() == 0) {
235                    args = new Args();
236                } else {
237                    args = mArgsPool.pop();
238                }
239                args.unique = unique;
240                args.inode = inode;
241                args.offset = offset;
242                args.size = size;
243                args.data = data;
244                args.entry = getCallbackEntryOrThrowLocked(inode);
245                if (!args.entry.handler.sendMessage(
246                        Message.obtain(args.entry.handler, command, 0, 0, args))) {
247                    throw new ErrnoException("onCommand", OsConstants.EBADF);
248                }
249            } catch (Exception error) {
250                replySimpleLocked(unique, getError(error));
251            }
252        }
253    }
254
255    // Called by JNI.
256    @SuppressWarnings("unused")
257    private byte[] onOpen(long unique, long inode) {
258        synchronized (mLock) {
259            try {
260                final CallbackEntry entry = getCallbackEntryOrThrowLocked(inode);
261                if (entry.opened) {
262                    throw new ErrnoException("onOpen", OsConstants.EMFILE);
263                }
264                if (mInstance != 0) {
265                    native_replyOpen(mInstance, unique, /* fh */ inode);
266                    entry.opened = true;
267                    return mBytesMap.startUsing(entry.getThreadId());
268                }
269            } catch (ErrnoException error) {
270                replySimpleLocked(unique, getError(error));
271            }
272            return null;
273        }
274    }
275
276    private static int getError(@NonNull Exception error) {
277        if (error instanceof ErrnoException) {
278            final int errno = ((ErrnoException) error).errno;
279            if (errno != OsConstants.ENOSYS) {
280                return -errno;
281            }
282        }
283        return -OsConstants.EBADF;
284    }
285
286    private CallbackEntry getCallbackEntryOrThrowLocked(long inode) throws ErrnoException {
287        final CallbackEntry entry = mCallbackMap.get(checkInode(inode));
288        if (entry == null) {
289            throw new ErrnoException("getCallbackEntryOrThrowLocked", OsConstants.ENOENT);
290        }
291        return entry;
292    }
293
294    private void recycleLocked(Args args) {
295        if (mArgsPool.size() < ARGS_POOL_SIZE) {
296            mArgsPool.add(args);
297        }
298    }
299
300    private void replySimpleLocked(long unique, int result) {
301        if (mInstance != 0) {
302            native_replySimple(mInstance, unique, result);
303        }
304    }
305
306    native long native_new(int fd);
307    native void native_delete(long ptr);
308    native void native_start(long ptr);
309
310    native void native_replySimple(long ptr, long unique, int result);
311    native void native_replyOpen(long ptr, long unique, long fh);
312    native void native_replyLookup(long ptr, long unique, long inode, long size);
313    native void native_replyGetAttr(long ptr, long unique, long inode, long size);
314    native void native_replyWrite(long ptr, long unique, int size);
315    native void native_replyRead(long ptr, long unique, int size, byte[] bytes);
316
317    private static int checkInode(long inode) {
318        Preconditions.checkArgumentInRange(inode, MIN_INODE, Integer.MAX_VALUE, "checkInode");
319        return (int) inode;
320    }
321
322    public static class UnmountedException extends Exception {}
323
324    private static class CallbackEntry {
325        final ProxyFileDescriptorCallback callback;
326        final Handler handler;
327        boolean opened;
328
329        CallbackEntry(ProxyFileDescriptorCallback callback, Handler handler) {
330            this.callback = Preconditions.checkNotNull(callback);
331            this.handler = Preconditions.checkNotNull(handler);
332        }
333
334        long getThreadId() {
335            return handler.getLooper().getThread().getId();
336        }
337    }
338
339    /**
340     * Entry for bytes map.
341     */
342    private static class BytesMapEntry {
343        int counter = 0;
344        byte[] bytes = new byte[FUSE_MAX_WRITE];
345    }
346
347    /**
348     * Map between Thread ID and byte buffer.
349     */
350    private static class BytesMap {
351        final Map<Long, BytesMapEntry> mEntries = new HashMap<>();
352
353        byte[] startUsing(long threadId) {
354            BytesMapEntry entry = mEntries.get(threadId);
355            if (entry == null) {
356                entry = new BytesMapEntry();
357                mEntries.put(threadId, entry);
358            }
359            entry.counter++;
360            return entry.bytes;
361        }
362
363        void stopUsing(long threadId) {
364            final BytesMapEntry entry = mEntries.get(threadId);
365            Preconditions.checkNotNull(entry);
366            entry.counter--;
367            if (entry.counter <= 0) {
368                mEntries.remove(threadId);
369            }
370        }
371
372        void clear() {
373            mEntries.clear();
374        }
375    }
376
377    private static class Args {
378        long unique;
379        long inode;
380        long offset;
381        int size;
382        byte[] data;
383        CallbackEntry entry;
384    }
385}
386