FuseAppLoop.java revision 9fb00183a04036a58ee208f5bfd6c9768982f0aa
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.ParcelFileDescriptor;
23import android.system.ErrnoException;
24import android.system.OsConstants;
25import android.util.Log;
26import android.util.SparseArray;
27import com.android.internal.annotations.GuardedBy;
28import com.android.internal.annotations.VisibleForTesting;
29import com.android.internal.util.Preconditions;
30
31import java.io.IOException;
32import java.util.concurrent.ThreadFactory;
33
34public class FuseAppLoop {
35    private static final String TAG = "FuseAppLoop";
36    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
37    public static final int ROOT_INODE = 1;
38    private static final int MIN_INODE = 2;
39    private static final ThreadFactory sDefaultThreadFactory = new ThreadFactory() {
40        @Override
41        public Thread newThread(Runnable r) {
42            return new Thread(r, TAG);
43        }
44    };
45
46    private final Object mLock = new Object();
47    private final int mMountPointId;
48    private final Thread mThread;
49
50    @GuardedBy("mLock")
51    private final SparseArray<CallbackEntry> mCallbackMap = new SparseArray<>();
52
53    /**
54     * Sequential number can be used as file name and inode in AppFuse.
55     * 0 is regarded as an error, 1 is mount point. So we start the number from 2.
56     */
57    @GuardedBy("mLock")
58    private int mNextInode = MIN_INODE;
59
60    private FuseAppLoop(
61            int mountPointId, @NonNull ParcelFileDescriptor fd, @Nullable ThreadFactory factory) {
62        mMountPointId = mountPointId;
63        final int rawFd = fd.detachFd();
64        if (factory == null) {
65            factory = sDefaultThreadFactory;
66        }
67        mThread = factory.newThread(new Runnable() {
68            @Override
69            public void run() {
70                // rawFd is closed by native_start_loop. Java code does not need to close it.
71                native_start_loop(rawFd);
72            }
73        });
74    }
75
76    public static @NonNull FuseAppLoop open(int mountPointId, @NonNull ParcelFileDescriptor fd,
77            @Nullable ThreadFactory factory) {
78        Preconditions.checkNotNull(fd);
79        final FuseAppLoop loop = new FuseAppLoop(mountPointId, fd, factory);
80        loop.mThread.start();
81        return loop;
82    }
83
84    public int registerCallback(@NonNull ProxyFileDescriptorCallback callback)
85            throws UnmountedException, IOException {
86        if (mThread.getState() == Thread.State.TERMINATED) {
87            throw new UnmountedException();
88        }
89        synchronized (mLock) {
90            if (mCallbackMap.size() >= Integer.MAX_VALUE - MIN_INODE) {
91                throw new IOException("Too many opened files.");
92            }
93            int id;
94            while (true) {
95                id = mNextInode;
96                mNextInode++;
97                if (mNextInode < 0) {
98                    mNextInode = MIN_INODE;
99                }
100                if (mCallbackMap.get(id) == null) {
101                    break;
102                }
103            }
104            mCallbackMap.put(id, new CallbackEntry(callback));
105            return id;
106        }
107    }
108
109    public void unregisterCallback(int id) {
110        mCallbackMap.remove(id);
111    }
112
113    public int getMountPointId() {
114        return mMountPointId;
115    }
116
117    private CallbackEntry getCallbackEntryOrThrowLocked(long inode) throws ErrnoException {
118        final CallbackEntry entry = mCallbackMap.get(checkInode(inode));
119        if (entry != null) {
120            return entry;
121        } else {
122            throw new ErrnoException("getCallbackEntry", OsConstants.ENOENT);
123        }
124    }
125
126    // Called by JNI.
127    @SuppressWarnings("unused")
128    private long onGetSize(long inode) {
129        synchronized(mLock) {
130            try {
131                return getCallbackEntryOrThrowLocked(inode).callback.onGetSize();
132            } catch (ErrnoException exp) {
133                return getError(exp);
134            }
135        }
136    }
137
138    // Called by JNI.
139    @SuppressWarnings("unused")
140    private int onOpen(long inode) {
141        synchronized(mLock) {
142            try {
143                final CallbackEntry entry = getCallbackEntryOrThrowLocked(inode);
144                if (entry.opened) {
145                    throw new ErrnoException("onOpen", OsConstants.EMFILE);
146                }
147                entry.opened = true;
148                // Use inode as file handle. It's OK because AppFuse does not allow to open the same
149                // file twice.
150                return (int) inode;
151            } catch (ErrnoException exp) {
152                return getError(exp);
153            }
154        }
155    }
156
157    // Called by JNI.
158    @SuppressWarnings("unused")
159    private int onFsync(long inode) {
160        synchronized(mLock) {
161            try {
162                getCallbackEntryOrThrowLocked(inode).callback.onFsync();
163                return 0;
164            } catch (ErrnoException exp) {
165                return getError(exp);
166            }
167        }
168    }
169
170    // Called by JNI.
171    @SuppressWarnings("unused")
172    private int onRelease(long inode) {
173        synchronized(mLock) {
174            try {
175                getCallbackEntryOrThrowLocked(inode).callback.onRelease();
176                return 0;
177            } catch (ErrnoException exp) {
178                return getError(exp);
179            } finally {
180                mCallbackMap.remove(checkInode(inode));
181            }
182        }
183    }
184
185    // Called by JNI.
186    @SuppressWarnings("unused")
187    private int onRead(long inode, long offset, int size, byte[] bytes) {
188        synchronized(mLock) {
189            try {
190                return getCallbackEntryOrThrowLocked(inode).callback.onRead(offset, size, bytes);
191            } catch (ErrnoException exp) {
192                return getError(exp);
193            }
194        }
195    }
196
197    // Called by JNI.
198    @SuppressWarnings("unused")
199    private int onWrite(long inode, long offset, int size, byte[] bytes) {
200        synchronized(mLock) {
201            try {
202                return getCallbackEntryOrThrowLocked(inode).callback.onWrite(offset, size, bytes);
203            } catch (ErrnoException exp) {
204                return getError(exp);
205            }
206        }
207    }
208
209    private static int getError(@NonNull ErrnoException exp) {
210        // Should not return ENOSYS because the kernel stops
211        // dispatching the FUSE action once FUSE implementation returns ENOSYS for the action.
212        return exp.errno != OsConstants.ENOSYS ? -exp.errno : -OsConstants.EIO;
213    }
214
215    native boolean native_start_loop(int fd);
216
217    private static int checkInode(long inode) {
218        Preconditions.checkArgumentInRange(inode, MIN_INODE, Integer.MAX_VALUE, "checkInode");
219        return (int) inode;
220    }
221
222    public static class UnmountedException extends Exception {}
223
224    private static class CallbackEntry {
225        final ProxyFileDescriptorCallback callback;
226        boolean opened;
227        CallbackEntry(ProxyFileDescriptorCallback callback) {
228            Preconditions.checkNotNull(callback);
229            this.callback = callback;
230        }
231    }
232}
233