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.server.storage; 18 19 import static android.service.storage.ExternalStorageService.EXTRA_ERROR; 20 import static android.service.storage.ExternalStorageService.FLAG_SESSION_ATTRIBUTE_INDEXABLE; 21 import static android.service.storage.ExternalStorageService.FLAG_SESSION_TYPE_FUSE; 22 23 import static com.android.server.storage.StorageSessionController.ExternalStorageServiceException; 24 25 import android.annotation.MainThread; 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.ServiceConnection; 32 import android.os.Bundle; 33 import android.os.HandlerThread; 34 import android.os.IBinder; 35 import android.os.ParcelFileDescriptor; 36 import android.os.ParcelableException; 37 import android.os.RemoteCallback; 38 import android.os.RemoteException; 39 import android.os.UserHandle; 40 import android.os.storage.StorageManager; 41 import android.os.storage.StorageManagerInternal; 42 import android.os.storage.StorageVolume; 43 import android.service.storage.ExternalStorageService; 44 import android.service.storage.IExternalStorageService; 45 import android.util.Slog; 46 import android.util.SparseArray; 47 48 import com.android.internal.annotations.GuardedBy; 49 import com.android.internal.util.Preconditions; 50 import com.android.server.LocalServices; 51 52 import java.io.IOException; 53 import java.util.ArrayList; 54 import java.util.HashMap; 55 import java.util.HashSet; 56 import java.util.List; 57 import java.util.Map; 58 import java.util.Objects; 59 import java.util.Set; 60 import java.util.concurrent.CompletableFuture; 61 import java.util.concurrent.TimeUnit; 62 import java.util.function.Consumer; 63 64 /** 65 * Controls the lifecycle of the {@link ActiveConnection} to an {@link ExternalStorageService} 66 * for a user and manages storage sessions associated with mounted volumes. 67 */ 68 public final class StorageUserConnection { 69 private static final String TAG = "StorageUserConnection"; 70 71 private static final int DEFAULT_REMOTE_TIMEOUT_SECONDS = 20; 72 73 private final Object mSessionsLock = new Object(); 74 private final Context mContext; 75 private final int mUserId; 76 private final StorageSessionController mSessionController; 77 private final StorageManagerInternal mSmInternal; 78 private final ActiveConnection mActiveConnection = new ActiveConnection(); 79 @GuardedBy("mSessionsLock") private final Map<String, Session> mSessions = new HashMap<>(); 80 @GuardedBy("mSessionsLock") private final SparseArray<Integer> mUidsBlockedOnIo = new SparseArray<>(); 81 private final HandlerThread mHandlerThread; 82 StorageUserConnection(Context context, int userId, StorageSessionController controller)83 public StorageUserConnection(Context context, int userId, StorageSessionController controller) { 84 mContext = Objects.requireNonNull(context); 85 mUserId = Preconditions.checkArgumentNonnegative(userId); 86 mSessionController = controller; 87 mSmInternal = LocalServices.getService(StorageManagerInternal.class); 88 mHandlerThread = new HandlerThread("StorageUserConnectionThread-" + mUserId); 89 mHandlerThread.start(); 90 } 91 92 /** 93 * Creates and starts a storage {@link Session}. 94 * 95 * They must also be cleaned up with {@link #removeSession}. 96 * 97 * @throws IllegalArgumentException if a {@code Session} with {@code sessionId} already exists 98 */ startSession(String sessionId, ParcelFileDescriptor pfd, String upperPath, String lowerPath)99 public void startSession(String sessionId, ParcelFileDescriptor pfd, String upperPath, 100 String lowerPath) throws ExternalStorageServiceException { 101 Objects.requireNonNull(sessionId); 102 Objects.requireNonNull(pfd); 103 Objects.requireNonNull(upperPath); 104 Objects.requireNonNull(lowerPath); 105 106 Session session = new Session(sessionId, upperPath, lowerPath); 107 synchronized (mSessionsLock) { 108 Preconditions.checkArgument(!mSessions.containsKey(sessionId)); 109 mSessions.put(sessionId, session); 110 } 111 mActiveConnection.startSession(session, pfd); 112 } 113 114 /** 115 * Notifies Storage Service about volume state changed. 116 * 117 * @throws ExternalStorageServiceException if failed to notify the Storage Service that 118 * {@code StorageVolume} is changed 119 */ notifyVolumeStateChanged(String sessionId, StorageVolume vol)120 public void notifyVolumeStateChanged(String sessionId, StorageVolume vol) 121 throws ExternalStorageServiceException { 122 Objects.requireNonNull(sessionId); 123 Objects.requireNonNull(vol); 124 125 synchronized (mSessionsLock) { 126 if (!mSessions.containsKey(sessionId)) { 127 Slog.i(TAG, "No session found for sessionId: " + sessionId); 128 return; 129 } 130 } 131 mActiveConnection.notifyVolumeStateChanged(sessionId, vol); 132 } 133 134 /** 135 * Frees any cache held by ExternalStorageService. 136 * 137 * <p> Blocks until the service frees the cache or fails in doing so. 138 * 139 * @param volumeUuid uuid of the {@link StorageVolume} from which cache needs to be freed 140 * @param bytes number of bytes which need to be freed 141 * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService 142 */ freeCache(String volumeUuid, long bytes)143 public void freeCache(String volumeUuid, long bytes) 144 throws ExternalStorageServiceException { 145 synchronized (mSessionsLock) { 146 for (String sessionId : mSessions.keySet()) { 147 mActiveConnection.freeCache(sessionId, volumeUuid, bytes); 148 } 149 } 150 } 151 152 /** 153 * Called when {@code packageName} is about to ANR 154 * 155 * @return ANR dialog delay in milliseconds 156 */ notifyAnrDelayStarted(String packageName, int uid, int tid, int reason)157 public void notifyAnrDelayStarted(String packageName, int uid, int tid, int reason) 158 throws ExternalStorageServiceException { 159 List<String> primarySessionIds = mSmInternal.getPrimaryVolumeIds(); 160 synchronized (mSessionsLock) { 161 for (String sessionId : mSessions.keySet()) { 162 if (primarySessionIds.contains(sessionId)) { 163 mActiveConnection.notifyAnrDelayStarted(packageName, uid, tid, reason); 164 return; 165 } 166 } 167 } 168 } 169 170 /** 171 * Removes a session without ending it or waiting for exit. 172 * 173 * This should only be used if the session has certainly been ended because the volume was 174 * unmounted or the user running the session has been stopped. Otherwise, wait for session 175 * with {@link #waitForExit}. 176 **/ removeSession(String sessionId)177 public Session removeSession(String sessionId) { 178 synchronized (mSessionsLock) { 179 mUidsBlockedOnIo.clear(); 180 return mSessions.remove(sessionId); 181 } 182 } 183 184 /** 185 * Removes a session and waits for exit 186 * 187 * @throws ExternalStorageServiceException if the session may not have exited 188 **/ removeSessionAndWait(String sessionId)189 public void removeSessionAndWait(String sessionId) throws ExternalStorageServiceException { 190 Session session = removeSession(sessionId); 191 if (session == null) { 192 Slog.i(TAG, "No session found for id: " + sessionId); 193 return; 194 } 195 196 Slog.i(TAG, "Waiting for session end " + session + " ..."); 197 mActiveConnection.endSession(session); 198 } 199 200 /** Restarts all available sessions for a user without blocking. 201 * 202 * Any failures will be ignored. 203 **/ resetUserSessions()204 public void resetUserSessions() { 205 synchronized (mSessionsLock) { 206 if (mSessions.isEmpty()) { 207 // Nothing to reset if we have no sessions to restart; we typically 208 // hit this path if the user was consciously shut down. 209 return; 210 } 211 } 212 mSmInternal.resetUser(mUserId); 213 } 214 215 /** 216 * Removes all sessions, without waiting. 217 */ removeAllSessions()218 public void removeAllSessions() { 219 synchronized (mSessionsLock) { 220 Slog.i(TAG, "Removing " + mSessions.size() + " sessions for user: " + mUserId + "..."); 221 mSessions.clear(); 222 } 223 } 224 225 /** 226 * Closes the connection to the {@link ExternalStorageService}. The connection will typically 227 * be restarted after close. 228 */ close()229 public void close() { 230 mActiveConnection.close(); 231 mHandlerThread.quit(); 232 } 233 234 /** Returns all created sessions. */ getAllSessionIds()235 public Set<String> getAllSessionIds() { 236 synchronized (mSessionsLock) { 237 return new HashSet<>(mSessions.keySet()); 238 } 239 } 240 241 /** 242 * Notify the controller that an app with {@code uid} and {@code tid} is blocked on an IO 243 * request on {@code volumeUuid} for {@code reason}. 244 * 245 * This blocked state can be queried with {@link #isAppIoBlocked} 246 * 247 * @hide 248 */ notifyAppIoBlocked(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)249 public void notifyAppIoBlocked(String volumeUuid, int uid, int tid, 250 @StorageManager.AppIoBlockedReason int reason) { 251 synchronized (mSessionsLock) { 252 int ioBlockedCounter = mUidsBlockedOnIo.get(uid, 0); 253 mUidsBlockedOnIo.put(uid, ++ioBlockedCounter); 254 } 255 } 256 257 /** 258 * Notify the connection that an app with {@code uid} and {@code tid} has resmed a previously 259 * blocked IO request on {@code volumeUuid} for {@code reason}. 260 * 261 * All app IO will be automatically marked as unblocked if {@code volumeUuid} is unmounted. 262 */ notifyAppIoResumed(String volumeUuid, int uid, int tid, @StorageManager.AppIoBlockedReason int reason)263 public void notifyAppIoResumed(String volumeUuid, int uid, int tid, 264 @StorageManager.AppIoBlockedReason int reason) { 265 synchronized (mSessionsLock) { 266 int ioBlockedCounter = mUidsBlockedOnIo.get(uid, 0); 267 if (ioBlockedCounter == 0) { 268 Slog.w(TAG, "Unexpected app IO resumption for uid: " + uid); 269 } 270 271 if (ioBlockedCounter <= 1) { 272 mUidsBlockedOnIo.remove(uid); 273 } else { 274 mUidsBlockedOnIo.put(uid, --ioBlockedCounter); 275 } 276 } 277 } 278 279 /** Returns {@code true} if {@code uid} is blocked on IO, {@code false} otherwise */ isAppIoBlocked(int uid)280 public boolean isAppIoBlocked(int uid) { 281 synchronized (mSessionsLock) { 282 return mUidsBlockedOnIo.contains(uid); 283 } 284 } 285 286 @FunctionalInterface 287 interface AsyncStorageServiceCall { run(@onNull IExternalStorageService service, RemoteCallback callback)288 void run(@NonNull IExternalStorageService service, RemoteCallback callback) throws 289 RemoteException; 290 } 291 292 private final class ActiveConnection implements AutoCloseable { 293 private final Object mLock = new Object(); 294 295 // Lifecycle connection to the external storage service, needed to unbind. 296 @GuardedBy("mLock") @Nullable private ServiceConnection mServiceConnection; 297 298 // A future that holds the remote interface 299 @GuardedBy("mLock") 300 @Nullable private CompletableFuture<IExternalStorageService> mRemoteFuture; 301 302 // A list of outstanding futures for async calls, for which we are still waiting 303 // for a callback. Used to unblock waiters if the service dies. 304 @GuardedBy("mLock") 305 private final ArrayList<CompletableFuture<Void>> mOutstandingOps = new ArrayList<>(); 306 307 @Override close()308 public void close() { 309 ServiceConnection oldConnection = null; 310 synchronized (mLock) { 311 Slog.i(TAG, "Closing connection for user " + mUserId); 312 oldConnection = mServiceConnection; 313 mServiceConnection = null; 314 if (mRemoteFuture != null) { 315 // Let folks who are waiting for the connection know it ain't gonna happen 316 mRemoteFuture.cancel(true); 317 mRemoteFuture = null; 318 } 319 // Let folks waiting for callbacks from the remote know it ain't gonna happen 320 for (CompletableFuture<Void> op : mOutstandingOps) { 321 op.cancel(true); 322 } 323 mOutstandingOps.clear(); 324 } 325 326 if (oldConnection != null) { 327 try { 328 mContext.unbindService(oldConnection); 329 } catch (Exception e) { 330 // Handle IllegalArgumentException that may be thrown if the user is already 331 // stopped when we try to unbind 332 Slog.w(TAG, "Failed to unbind service", e); 333 } 334 } 335 } 336 asyncBestEffort(Consumer<IExternalStorageService> consumer)337 private void asyncBestEffort(Consumer<IExternalStorageService> consumer) { 338 synchronized (mLock) { 339 if (mRemoteFuture == null) { 340 Slog.w(TAG, "Dropping async request service is not bound"); 341 return; 342 } 343 344 IExternalStorageService service = mRemoteFuture.getNow(null); 345 if (service == null) { 346 Slog.w(TAG, "Dropping async request service is not connected"); 347 return; 348 } 349 350 consumer.accept(service); 351 } 352 } 353 waitForAsyncVoid(AsyncStorageServiceCall asyncCall)354 private void waitForAsyncVoid(AsyncStorageServiceCall asyncCall) throws Exception { 355 CompletableFuture<Void> opFuture = new CompletableFuture<>(); 356 RemoteCallback callback = new RemoteCallback(result -> setResult(result, opFuture)); 357 358 waitForAsync(asyncCall, callback, opFuture, mOutstandingOps, 359 DEFAULT_REMOTE_TIMEOUT_SECONDS); 360 } 361 waitForAsync(AsyncStorageServiceCall asyncCall, RemoteCallback callback, CompletableFuture<T> opFuture, ArrayList<CompletableFuture<T>> outstandingOps, long timeoutSeconds)362 private <T> T waitForAsync(AsyncStorageServiceCall asyncCall, RemoteCallback callback, 363 CompletableFuture<T> opFuture, ArrayList<CompletableFuture<T>> outstandingOps, 364 long timeoutSeconds) throws Exception { 365 CompletableFuture<IExternalStorageService> serviceFuture = connectIfNeeded(); 366 367 try { 368 synchronized (mLock) { 369 outstandingOps.add(opFuture); 370 } 371 return serviceFuture.thenCompose(service -> { 372 try { 373 asyncCall.run(service, callback); 374 } catch (RemoteException e) { 375 opFuture.completeExceptionally(e); 376 } 377 378 return opFuture; 379 }).get(timeoutSeconds, TimeUnit.SECONDS); 380 } finally { 381 synchronized (mLock) { 382 outstandingOps.remove(opFuture); 383 } 384 } 385 } 386 startSession(Session session, ParcelFileDescriptor fd)387 public void startSession(Session session, ParcelFileDescriptor fd) 388 throws ExternalStorageServiceException { 389 try { 390 waitForAsyncVoid((service, callback) -> service.startSession(session.sessionId, 391 FLAG_SESSION_TYPE_FUSE | FLAG_SESSION_ATTRIBUTE_INDEXABLE, 392 fd, session.upperPath, session.lowerPath, callback)); 393 } catch (Exception e) { 394 throw new ExternalStorageServiceException("Failed to start session: " + session, e); 395 } finally { 396 try { 397 fd.close(); 398 } catch (IOException e) { 399 // Ignore 400 } 401 } 402 } 403 endSession(Session session)404 public void endSession(Session session) throws ExternalStorageServiceException { 405 try { 406 waitForAsyncVoid((service, callback) -> 407 service.endSession(session.sessionId, callback)); 408 } catch (Exception e) { 409 throw new ExternalStorageServiceException("Failed to end session: " + session, e); 410 } 411 } 412 413 notifyVolumeStateChanged(String sessionId, StorageVolume vol)414 public void notifyVolumeStateChanged(String sessionId, StorageVolume vol) throws 415 ExternalStorageServiceException { 416 try { 417 waitForAsyncVoid((service, callback) -> 418 service.notifyVolumeStateChanged(sessionId, vol, callback)); 419 } catch (Exception e) { 420 throw new ExternalStorageServiceException("Failed to notify volume state changed " 421 + "for vol : " + vol, e); 422 } 423 } 424 freeCache(String sessionId, String volumeUuid, long bytes)425 public void freeCache(String sessionId, String volumeUuid, long bytes) 426 throws ExternalStorageServiceException { 427 try { 428 waitForAsyncVoid((service, callback) -> 429 service.freeCache(sessionId, volumeUuid, bytes, callback)); 430 } catch (Exception e) { 431 throw new ExternalStorageServiceException("Failed to free " + bytes 432 + " bytes for volumeUuid : " + volumeUuid, e); 433 } 434 } 435 notifyAnrDelayStarted(String packgeName, int uid, int tid, int reason)436 public void notifyAnrDelayStarted(String packgeName, int uid, int tid, int reason) 437 throws ExternalStorageServiceException { 438 asyncBestEffort(service -> { 439 try { 440 service.notifyAnrDelayStarted(packgeName, uid, tid, reason); 441 } catch (RemoteException e) { 442 Slog.w(TAG, "Failed to notify ANR delay started", e); 443 } 444 }); 445 } 446 setResult(Bundle result, CompletableFuture<Void> future)447 private void setResult(Bundle result, CompletableFuture<Void> future) { 448 ParcelableException ex = result.getParcelable(EXTRA_ERROR, android.os.ParcelableException.class); 449 if (ex != null) { 450 future.completeExceptionally(ex); 451 } else { 452 future.complete(null); 453 } 454 } 455 connectIfNeeded()456 private CompletableFuture<IExternalStorageService> connectIfNeeded() throws 457 ExternalStorageServiceException { 458 ComponentName name = mSessionController.getExternalStorageServiceComponentName(); 459 if (name == null) { 460 // Not ready to bind 461 throw new ExternalStorageServiceException( 462 "Not ready to bind to the ExternalStorageService for user " + mUserId); 463 } 464 synchronized (mLock) { 465 if (mRemoteFuture != null) { 466 return mRemoteFuture; 467 } 468 CompletableFuture<IExternalStorageService> future = new CompletableFuture<>(); 469 mServiceConnection = new ServiceConnection() { 470 @Override 471 public void onServiceConnected(ComponentName name, IBinder service) { 472 Slog.i(TAG, "Service: [" + name + "] connected. User [" + mUserId + "]"); 473 handleConnection(service); 474 } 475 476 @Override 477 @MainThread 478 public void onServiceDisconnected(ComponentName name) { 479 // Service crashed or process was killed, #onServiceConnected will be called 480 // Don't need to re-bind. 481 Slog.i(TAG, "Service: [" + name + "] disconnected. User [" + mUserId + "]"); 482 handleDisconnection(); 483 } 484 485 @Override 486 public void onBindingDied(ComponentName name) { 487 // Application hosting service probably got updated 488 // Need to re-bind. 489 Slog.i(TAG, "Service: [" + name + "] died. User [" + mUserId + "]"); 490 handleDisconnection(); 491 } 492 493 @Override 494 public void onNullBinding(ComponentName name) { 495 Slog.wtf(TAG, "Service: [" + name + "] is null. User [" + mUserId + "]"); 496 } 497 498 private void handleConnection(IBinder service) { 499 synchronized (mLock) { 500 future.complete( 501 IExternalStorageService.Stub.asInterface(service)); 502 } 503 } 504 505 private void handleDisconnection() { 506 // Clear all sessions because we will need a new device fd since 507 // StorageManagerService will reset the device mount state and #startSession 508 // will be called for any required mounts. 509 // Notify StorageManagerService so it can restart all necessary sessions 510 close(); 511 resetUserSessions(); 512 } 513 }; 514 515 Slog.i(TAG, "Binding to the ExternalStorageService for user " + mUserId); 516 // Schedule on a worker thread, because the system server main thread can be 517 // very busy early in boot. 518 if (mContext.bindServiceAsUser(new Intent().setComponent(name), 519 mServiceConnection, 520 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, 521 mHandlerThread.getThreadHandler(), 522 UserHandle.of(mUserId))) { 523 Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId); 524 mRemoteFuture = future; 525 return future; 526 } else { 527 throw new ExternalStorageServiceException( 528 "Failed to bind to the ExternalStorageService for user " + mUserId); 529 } 530 } 531 } 532 } 533 534 private static final class Session { 535 public final String sessionId; 536 public final String lowerPath; 537 public final String upperPath; 538 539 Session(String sessionId, String upperPath, String lowerPath) { 540 this.sessionId = sessionId; 541 this.upperPath = upperPath; 542 this.lowerPath = lowerPath; 543 } 544 545 @Override 546 public String toString() { 547 return "[SessionId: " + sessionId + ". UpperPath: " + upperPath + ". LowerPath: " 548 + lowerPath + "]"; 549 } 550 } 551 } 552