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 354 waitForAsyncVoid(AsyncStorageServiceCall asyncCall)355 private void waitForAsyncVoid(AsyncStorageServiceCall asyncCall) throws Exception { 356 waitForAsyncVoid(asyncCall, /*bindIfNotConnected*/ true, 357 DEFAULT_REMOTE_TIMEOUT_SECONDS); 358 } 359 waitForAsyncVoid(AsyncStorageServiceCall asyncCall, boolean bindIfNotConnected, int timeoutSeconds)360 private void waitForAsyncVoid(AsyncStorageServiceCall asyncCall, 361 boolean bindIfNotConnected, int timeoutSeconds) throws Exception { 362 CompletableFuture<Void> opFuture = new CompletableFuture<>(); 363 RemoteCallback callback = new RemoteCallback(result -> setResult(result, opFuture)); 364 365 waitForAsync(asyncCall, callback, opFuture, mOutstandingOps, bindIfNotConnected, 366 timeoutSeconds); 367 } 368 waitForAsync(AsyncStorageServiceCall asyncCall, RemoteCallback callback, CompletableFuture<T> opFuture, ArrayList<CompletableFuture<T>> outstandingOps, boolean bindIfNotConnected, long timeoutSeconds)369 private <T> T waitForAsync(AsyncStorageServiceCall asyncCall, RemoteCallback callback, 370 CompletableFuture<T> opFuture, ArrayList<CompletableFuture<T>> outstandingOps, 371 boolean bindIfNotConnected, long timeoutSeconds) throws Exception { 372 373 CompletableFuture<IExternalStorageService> serviceFuture; 374 if (bindIfNotConnected) { 375 serviceFuture = connectIfNeeded(); 376 } else { 377 synchronized (mLock) { 378 if (mRemoteFuture == null || mRemoteFuture.getNow(null) == null) { 379 Slog.w(TAG, "Dropping async request as service is not connected" 380 + "and request doesn't require connecting"); 381 return null; 382 } 383 serviceFuture = mRemoteFuture; 384 } 385 } 386 387 try { 388 synchronized (mLock) { 389 outstandingOps.add(opFuture); 390 } 391 return serviceFuture.thenCompose(service -> { 392 try { 393 asyncCall.run(service, callback); 394 } catch (RemoteException e) { 395 opFuture.completeExceptionally(e); 396 } 397 398 return opFuture; 399 }).get(timeoutSeconds, TimeUnit.SECONDS); 400 } finally { 401 synchronized (mLock) { 402 outstandingOps.remove(opFuture); 403 } 404 } 405 } 406 startSession(Session session, ParcelFileDescriptor fd)407 public void startSession(Session session, ParcelFileDescriptor fd) 408 throws ExternalStorageServiceException { 409 try { 410 waitForAsyncVoid((service, callback) -> service.startSession(session.sessionId, 411 FLAG_SESSION_TYPE_FUSE | FLAG_SESSION_ATTRIBUTE_INDEXABLE, 412 fd, session.upperPath, session.lowerPath, callback)); 413 } catch (Exception e) { 414 throw new ExternalStorageServiceException("Failed to start session: " + session, e); 415 } finally { 416 try { 417 fd.close(); 418 } catch (IOException e) { 419 // Ignore 420 } 421 } 422 } 423 endSession(Session session)424 public void endSession(Session session) throws ExternalStorageServiceException { 425 try { 426 waitForAsyncVoid((service, callback) -> 427 service.endSession(session.sessionId, callback), 428 // endSession shouldn't be trying to bind to remote service if the service 429 // isn't connected already as this means that no previous mounting has been 430 // completed. 431 /*bindIfNotConnected*/ false, /*timeoutSeconds*/ 10); 432 } catch (Exception e) { 433 throw new ExternalStorageServiceException("Failed to end session: " + session, e); 434 } 435 } 436 437 notifyVolumeStateChanged(String sessionId, StorageVolume vol)438 public void notifyVolumeStateChanged(String sessionId, StorageVolume vol) throws 439 ExternalStorageServiceException { 440 try { 441 waitForAsyncVoid((service, callback) -> 442 service.notifyVolumeStateChanged(sessionId, vol, callback), 443 // notifyVolumeStateChanged shouldn't be trying to bind to remote service 444 // if the service isn't connected already as this means that 445 // no previous mounting has been completed 446 /*bindIfNotConnected*/ false, /*timeoutSeconds*/ 10); 447 } catch (Exception e) { 448 throw new ExternalStorageServiceException("Failed to notify volume state changed " 449 + "for vol : " + vol, e); 450 } 451 } 452 freeCache(String sessionId, String volumeUuid, long bytes)453 public void freeCache(String sessionId, String volumeUuid, long bytes) 454 throws ExternalStorageServiceException { 455 try { 456 waitForAsyncVoid((service, callback) -> 457 service.freeCache(sessionId, volumeUuid, bytes, callback)); 458 } catch (Exception e) { 459 throw new ExternalStorageServiceException("Failed to free " + bytes 460 + " bytes for volumeUuid : " + volumeUuid, e); 461 } 462 } 463 notifyAnrDelayStarted(String packgeName, int uid, int tid, int reason)464 public void notifyAnrDelayStarted(String packgeName, int uid, int tid, int reason) 465 throws ExternalStorageServiceException { 466 asyncBestEffort(service -> { 467 try { 468 service.notifyAnrDelayStarted(packgeName, uid, tid, reason); 469 } catch (RemoteException e) { 470 Slog.w(TAG, "Failed to notify ANR delay started", e); 471 } 472 }); 473 } 474 setResult(Bundle result, CompletableFuture<Void> future)475 private void setResult(Bundle result, CompletableFuture<Void> future) { 476 ParcelableException ex = result.getParcelable(EXTRA_ERROR, android.os.ParcelableException.class); 477 if (ex != null) { 478 future.completeExceptionally(ex); 479 } else { 480 future.complete(null); 481 } 482 } 483 connectIfNeeded()484 private CompletableFuture<IExternalStorageService> connectIfNeeded() throws 485 ExternalStorageServiceException { 486 ComponentName name = mSessionController.getExternalStorageServiceComponentName(); 487 if (name == null) { 488 // Not ready to bind 489 throw new ExternalStorageServiceException( 490 "Not ready to bind to the ExternalStorageService for user " + mUserId); 491 } 492 synchronized (mLock) { 493 if (mRemoteFuture != null) { 494 return mRemoteFuture; 495 } 496 CompletableFuture<IExternalStorageService> future = new CompletableFuture<>(); 497 mServiceConnection = new ServiceConnection() { 498 @Override 499 public void onServiceConnected(ComponentName name, IBinder service) { 500 Slog.i(TAG, "Service: [" + name + "] connected. User [" + mUserId + "]"); 501 handleConnection(service); 502 } 503 504 @Override 505 @MainThread 506 public void onServiceDisconnected(ComponentName name) { 507 // Service crashed or process was killed, #onServiceConnected will be called 508 // Don't need to re-bind. 509 Slog.i(TAG, "Service: [" + name + "] disconnected. User [" + mUserId + "]"); 510 handleDisconnection(); 511 } 512 513 @Override 514 public void onBindingDied(ComponentName name) { 515 // Application hosting service probably got updated 516 // Need to re-bind. 517 Slog.i(TAG, "Service: [" + name + "] died. User [" + mUserId + "]"); 518 handleDisconnection(); 519 } 520 521 @Override 522 public void onNullBinding(ComponentName name) { 523 Slog.wtf(TAG, "Service: [" + name + "] is null. User [" + mUserId + "]"); 524 } 525 526 private void handleConnection(IBinder service) { 527 synchronized (mLock) { 528 future.complete( 529 IExternalStorageService.Stub.asInterface(service)); 530 } 531 } 532 533 private void handleDisconnection() { 534 // Clear all sessions because we will need a new device fd since 535 // StorageManagerService will reset the device mount state and #startSession 536 // will be called for any required mounts. 537 // Notify StorageManagerService so it can restart all necessary sessions 538 close(); 539 resetUserSessions(); 540 } 541 }; 542 543 Slog.i(TAG, "Binding to the ExternalStorageService for user " + mUserId); 544 // Schedule on a worker thread, because the system server main thread can be 545 // very busy early in boot. 546 if (mContext.bindServiceAsUser(new Intent().setComponent(name), 547 mServiceConnection, 548 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, 549 mHandlerThread.getThreadHandler(), 550 UserHandle.of(mUserId))) { 551 Slog.i(TAG, "Bound to the ExternalStorageService for user " + mUserId); 552 mRemoteFuture = future; 553 return future; 554 } else { 555 throw new ExternalStorageServiceException( 556 "Failed to bind to the ExternalStorageService for user " + mUserId); 557 } 558 } 559 } 560 } 561 562 private static final class Session { 563 public final String sessionId; 564 public final String lowerPath; 565 public final String upperPath; 566 567 Session(String sessionId, String upperPath, String lowerPath) { 568 this.sessionId = sessionId; 569 this.upperPath = upperPath; 570 this.lowerPath = lowerPath; 571 } 572 573 @Override 574 public String toString() { 575 return "[SessionId: " + sessionId + ". UpperPath: " + upperPath + ". LowerPath: " 576 + lowerPath + "]"; 577 } 578 } 579 } 580