1 /*
2  * Copyright (C) 2019 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 
17 package com.android.providers.media.fuse;
18 
19 import android.os.ParcelFileDescriptor;
20 import android.util.Log;
21 
22 import androidx.annotation.NonNull;
23 
24 import com.android.internal.annotations.GuardedBy;
25 import com.android.providers.media.MediaProvider;
26 
27 import java.io.File;
28 import java.io.IOException;
29 import java.util.Objects;
30 
31 /**
32  * Starts a FUSE session to handle FUSE messages from the kernel.
33  */
34 public final class FuseDaemon extends Thread {
35     public static final String TAG = "FuseDaemonThread";
36     private static final int POLL_INTERVAL_MS = 1000;
37     private static final int POLL_COUNT = 5;
38 
39     private final Object mLock = new Object();
40     private final MediaProvider mMediaProvider;
41     private final int mFuseDeviceFd;
42     private final String mPath;
43     private final ExternalStorageServiceImpl mService;
44     @GuardedBy("mLock")
45     private long mPtr;
46 
FuseDaemon(@onNull MediaProvider mediaProvider, @NonNull ExternalStorageServiceImpl service, @NonNull ParcelFileDescriptor fd, @NonNull String sessionId, @NonNull String path)47     public FuseDaemon(@NonNull MediaProvider mediaProvider,
48             @NonNull ExternalStorageServiceImpl service, @NonNull ParcelFileDescriptor fd,
49             @NonNull String sessionId, @NonNull String path) {
50         mMediaProvider = Objects.requireNonNull(mediaProvider);
51         mService = Objects.requireNonNull(service);
52         setName(Objects.requireNonNull(sessionId));
53         mFuseDeviceFd = Objects.requireNonNull(fd).detachFd();
54         mPath = Objects.requireNonNull(path);
55     }
56 
57     /** Starts a FUSE session. Does not return until the lower filesystem is unmounted. */
58     @Override
run()59     public void run() {
60         final long ptr;
61         synchronized (mLock) {
62             mPtr = native_new(mMediaProvider);
63             if (mPtr == 0) {
64                 throw new IllegalStateException("Unable to create native FUSE daemon");
65             }
66             ptr = mPtr;
67         }
68 
69         Log.i(TAG, "Starting thread for " + getName() + " ...");
70         native_start(ptr, mFuseDeviceFd, mPath); // Blocks
71         Log.i(TAG, "Exiting thread for " + getName() + " ...");
72 
73         synchronized (mLock) {
74             native_delete(mPtr);
75             mPtr = 0;
76         }
77         mService.onExitSession(getName());
78         Log.i(TAG, "Exited thread for " + getName());
79     }
80 
81     @Override
start()82     public synchronized void start() {
83         super.start();
84 
85         // Wait for native_start
86         waitForStart();
87 
88         // Initialize device id
89         initializeDeviceId();
90     }
91 
initializeDeviceId()92     private void initializeDeviceId() {
93         synchronized (mLock) {
94             if (mPtr == 0) {
95                 Log.e(TAG, "initializeDeviceId failed, FUSE daemon unavailable");
96                 return;
97             }
98             String path = mMediaProvider.getFuseFile(new File(mPath)).getAbsolutePath();
99             native_initialize_device_id(mPtr, path);
100         }
101     }
102 
waitForStart()103     private void waitForStart() {
104         int count = POLL_COUNT;
105         while (count-- > 0) {
106             synchronized (mLock) {
107                 if (mPtr != 0 && native_is_started(mPtr)) {
108                     return;
109                 }
110             }
111             try {
112                 Log.v(TAG, "Waiting " + POLL_INTERVAL_MS + "ms for FUSE start. Count " + count);
113                 Thread.sleep(POLL_INTERVAL_MS);
114             } catch (InterruptedException e) {
115                 Thread.currentThread().interrupt();
116                 Log.e(TAG, "Interrupted while starting FUSE", e);
117             }
118         }
119         throw new IllegalStateException("Failed to start FUSE");
120     }
121 
122     /** Waits for any running FUSE sessions to return. */
waitForExit()123     public void waitForExit() {
124         int waitMs = POLL_COUNT * POLL_INTERVAL_MS;
125         Log.i(TAG, "Waiting " + waitMs + "ms for FUSE " + getName() + " to exit...");
126 
127         try {
128             join(waitMs);
129         } catch (InterruptedException e) {
130             Thread.currentThread().interrupt();
131             throw new IllegalStateException("Interrupted while terminating FUSE " + getName());
132         }
133 
134         if (isAlive()) {
135             throw new IllegalStateException("Failed to exit FUSE " + getName() + " successfully");
136         }
137 
138         Log.i(TAG, "Exited FUSE " + getName() + " successfully");
139     }
140 
141     /**
142      * Checks if file with {@code path} should be opened via FUSE to avoid cache inconcistencies.
143      * May place a F_RDLCK or F_WRLCK with fcntl(2) depending on {@code readLock}
144      *
145      * @return {@code true} if the file should be opened via FUSE, {@code false} otherwise
146      */
shouldOpenWithFuse(String path, boolean readLock, int fd)147     public boolean shouldOpenWithFuse(String path, boolean readLock, int fd) {
148         synchronized (mLock) {
149             if (mPtr == 0) {
150                 Log.i(TAG, "shouldOpenWithFuse failed, FUSE daemon unavailable");
151                 return false;
152             }
153             return native_should_open_with_fuse(mPtr, path, readLock, fd);
154         }
155     }
156 
157     /**
158      * Invalidates FUSE VFS dentry cache for {@code path}
159      */
invalidateFuseDentryCache(String path)160     public void invalidateFuseDentryCache(String path) {
161         synchronized (mLock) {
162             if (mPtr == 0) {
163                 Log.i(TAG, "invalidateFuseDentryCache failed, FUSE daemon unavailable");
164                 return;
165             }
166             native_invalidate_fuse_dentry_cache(mPtr, path);
167         }
168     }
169 
getOriginalMediaFormatFilePath(ParcelFileDescriptor fileDescriptor)170     public String getOriginalMediaFormatFilePath(ParcelFileDescriptor fileDescriptor)
171             throws IOException {
172         synchronized (mLock) {
173             if (mPtr == 0) {
174                 throw new IOException("FUSE daemon unavailable");
175             }
176             return native_get_original_media_format_file_path(mPtr, fileDescriptor.getFd());
177         }
178     }
179 
native_new(MediaProvider mediaProvider)180     private native long native_new(MediaProvider mediaProvider);
181 
182     // Takes ownership of the passed in file descriptor!
native_start(long daemon, int deviceFd, String path)183     private native void native_start(long daemon, int deviceFd, String path);
184 
native_delete(long daemon)185     private native void native_delete(long daemon);
native_should_open_with_fuse(long daemon, String path, boolean readLock, int fd)186     private native boolean native_should_open_with_fuse(long daemon, String path, boolean readLock,
187             int fd);
native_invalidate_fuse_dentry_cache(long daemon, String path)188     private native void native_invalidate_fuse_dentry_cache(long daemon, String path);
native_is_started(long daemon)189     private native boolean native_is_started(long daemon);
native_get_original_media_format_file_path(long daemon, int fd)190     private native String native_get_original_media_format_file_path(long daemon, int fd);
native_initialize_device_id(long daemon, String path)191     private native void native_initialize_device_id(long daemon, String path);
native_is_fuse_thread()192     public static native boolean native_is_fuse_thread();
193 }
194