1 /* 2 * Copyright 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 androidx.sqlite.inspection; 18 19 import static android.database.DatabaseUtils.getSqlStatementType; 20 21 import static androidx.sqlite.inspection.DatabaseExtensions.isAttemptAtUsingClosedDatabase; 22 import static androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_DB_CLOSED_DURING_OPERATION; 23 import static androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_ISSUE_WITH_LOCKING_DATABASE; 24 import static androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_ISSUE_WITH_PROCESSING_NEW_DATABASE_CONNECTION; 25 import static androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_ISSUE_WITH_PROCESSING_QUERY; 26 import static androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_NO_OPEN_DATABASE_WITH_REQUESTED_ID; 27 import static androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_UNKNOWN; 28 import static androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_UNRECOGNISED_COMMAND; 29 30 import android.annotation.SuppressLint; 31 import android.app.Application; 32 import android.database.Cursor; 33 import android.database.CursorWrapper; 34 import android.database.DatabaseUtils; 35 import android.database.sqlite.SQLiteClosable; 36 import android.database.sqlite.SQLiteCursor; 37 import android.database.sqlite.SQLiteCursorDriver; 38 import android.database.sqlite.SQLiteDatabase; 39 import android.database.sqlite.SQLiteException; 40 import android.database.sqlite.SQLiteQuery; 41 import android.database.sqlite.SQLiteStatement; 42 import android.os.Build; 43 import android.os.CancellationSignal; 44 import android.util.Log; 45 46 import androidx.inspection.ArtTooling; 47 import androidx.inspection.ArtTooling.EntryHook; 48 import androidx.inspection.ArtTooling.ExitHook; 49 import androidx.inspection.Connection; 50 import androidx.inspection.Inspector; 51 import androidx.inspection.InspectorEnvironment; 52 import androidx.sqlite.inspection.SqliteInspectorProtocol.AcquireDatabaseLockCommand; 53 import androidx.sqlite.inspection.SqliteInspectorProtocol.AcquireDatabaseLockResponse; 54 import androidx.sqlite.inspection.SqliteInspectorProtocol.CellValue; 55 import androidx.sqlite.inspection.SqliteInspectorProtocol.Column; 56 import androidx.sqlite.inspection.SqliteInspectorProtocol.Command; 57 import androidx.sqlite.inspection.SqliteInspectorProtocol.DatabaseClosedEvent; 58 import androidx.sqlite.inspection.SqliteInspectorProtocol.DatabaseOpenedEvent; 59 import androidx.sqlite.inspection.SqliteInspectorProtocol.DatabasePossiblyChangedEvent; 60 import androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent; 61 import androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode; 62 import androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorOccurredEvent; 63 import androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorOccurredResponse; 64 import androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorRecoverability; 65 import androidx.sqlite.inspection.SqliteInspectorProtocol.Event; 66 import androidx.sqlite.inspection.SqliteInspectorProtocol.GetSchemaCommand; 67 import androidx.sqlite.inspection.SqliteInspectorProtocol.GetSchemaResponse; 68 import androidx.sqlite.inspection.SqliteInspectorProtocol.KeepDatabasesOpenCommand; 69 import androidx.sqlite.inspection.SqliteInspectorProtocol.KeepDatabasesOpenResponse; 70 import androidx.sqlite.inspection.SqliteInspectorProtocol.QueryCommand; 71 import androidx.sqlite.inspection.SqliteInspectorProtocol.QueryParameterValue; 72 import androidx.sqlite.inspection.SqliteInspectorProtocol.QueryResponse; 73 import androidx.sqlite.inspection.SqliteInspectorProtocol.ReleaseDatabaseLockCommand; 74 import androidx.sqlite.inspection.SqliteInspectorProtocol.ReleaseDatabaseLockResponse; 75 import androidx.sqlite.inspection.SqliteInspectorProtocol.Response; 76 import androidx.sqlite.inspection.SqliteInspectorProtocol.Row; 77 import androidx.sqlite.inspection.SqliteInspectorProtocol.Table; 78 import androidx.sqlite.inspection.SqliteInspectorProtocol.TrackDatabasesResponse; 79 80 import com.google.protobuf.ByteString; 81 82 import org.jspecify.annotations.NonNull; 83 import org.jspecify.annotations.Nullable; 84 85 import java.io.File; 86 import java.io.PrintWriter; 87 import java.io.StringWriter; 88 import java.util.ArrayList; 89 import java.util.Arrays; 90 import java.util.Collections; 91 import java.util.HashSet; 92 import java.util.List; 93 import java.util.Map; 94 import java.util.Set; 95 import java.util.WeakHashMap; 96 import java.util.concurrent.Executor; 97 import java.util.concurrent.Future; 98 99 /** 100 * Inspector to work with SQLite databases 101 */ 102 @SuppressWarnings({"TryFinallyCanBeTryWithResources", "SameParameterValue"}) 103 final class SqliteInspector extends Inspector { 104 private static final String OPEN_DATABASE_COMMAND_SIGNATURE_API_11 = "openDatabase" 105 + "(" 106 + "Ljava/lang/String;" 107 + "Landroid/database/sqlite/SQLiteDatabase$CursorFactory;" 108 + "I" 109 + "Landroid/database/DatabaseErrorHandler;" 110 + ")" 111 + "Landroid/database/sqlite/SQLiteDatabase;"; 112 113 private static final String OPEN_DATABASE_COMMAND_SIGNATURE_API_27 = "openDatabase" 114 + "(" 115 + "Ljava/io/File;" 116 + "Landroid/database/sqlite/SQLiteDatabase$OpenParams;" 117 + ")" 118 + "Landroid/database/sqlite/SQLiteDatabase;"; 119 120 private static final String CREATE_IN_MEMORY_DATABASE_COMMAND_SIGNATURE_API_27 = 121 "createInMemory" 122 + "(" 123 + "Landroid/database/sqlite/SQLiteDatabase$OpenParams;" 124 + ")" 125 + "Landroid/database/sqlite/SQLiteDatabase;"; 126 127 private static final String ALL_REFERENCES_RELEASE_COMMAND_SIGNATURE = 128 "onAllReferencesReleased()V"; 129 130 // SQLiteStatement methods 131 private static final List<String> SQLITE_STATEMENT_EXECUTE_METHODS_SIGNATURES = Arrays.asList( 132 "execute()V", 133 "executeInsert()J", 134 "executeUpdateDelete()I"); 135 136 private static final int INVALIDATION_MIN_INTERVAL_MS = 1000; 137 138 // Note: this only works on API26+ because of pragma_* functions 139 // TODO: replace with a resource file 140 // language=SQLite 141 private static final String sQueryTableInfo = "select\n" 142 + " m.type as type,\n" 143 + " m.name as tableName,\n" 144 + " ti.name as columnName,\n" 145 + " ti.type as columnType,\n" 146 + " [notnull],\n" 147 + " pk,\n" 148 + " ifnull([unique], 0) as [unique]\n" 149 + "from sqlite_master AS m, pragma_table_info(m.name) as ti\n" 150 + "left outer join\n" 151 + " (\n" 152 + " select tableName, name as columnName, ti.[unique]\n" 153 + " from\n" 154 + " (\n" 155 + " select m.name as tableName, il.name as indexName, il.[unique]\n" 156 + " from\n" 157 + " sqlite_master AS m,\n" 158 + " pragma_index_list(m.name) AS il,\n" 159 + " pragma_index_info(il.name) as ii\n" 160 + " where il.[unique] = 1\n" 161 + " group by il.name\n" 162 + " having count(*) = 1 -- countOfColumnsInIndex=1\n" 163 + " )\n" 164 + " as ti, -- tableName|indexName|unique : unique=1 and " 165 + "countOfColumnsInIndex=1\n" 166 + " pragma_index_info(ti.indexName)\n" 167 + " )\n" 168 + " as tci -- tableName|columnName|unique : unique=1 and countOfColumnsInIndex=1\n" 169 + " on tci.tableName = m.name and tci.columnName = ti.name\n" 170 + "where m.type in ('table', 'view')\n" 171 + "order by type, tableName, ti.cid -- cid = columnId"; 172 173 private static final Set<String> sHiddenTables = new HashSet<>(Arrays.asList( 174 "android_metadata", "sqlite_sequence")); 175 176 private final DatabaseRegistry mDatabaseRegistry; 177 private final DatabaseLockRegistry mDatabaseLockRegistry; 178 private final InspectorEnvironment mEnvironment; 179 private final Executor mIOExecutor; 180 181 /** 182 * Utility instance that handles communication with Room's InvalidationTracker instances. 183 */ 184 private final RoomInvalidationRegistry mRoomInvalidationRegistry; 185 186 private final List<Invalidation> mInvalidations = new ArrayList<>(); 187 SqliteInspector(@onNull Connection connection, @NonNull InspectorEnvironment environment)188 SqliteInspector(@NonNull Connection connection, @NonNull InspectorEnvironment environment) { 189 super(connection); 190 mEnvironment = environment; 191 mIOExecutor = environment.executors().io(); 192 mRoomInvalidationRegistry = new RoomInvalidationRegistry(mEnvironment); 193 mInvalidations.add(mRoomInvalidationRegistry); 194 mInvalidations.add(SqlDelightInvalidation.create(mEnvironment.artTooling())); 195 mInvalidations.add(SqlDelight2Invalidation.create(mEnvironment.artTooling())); 196 197 mDatabaseRegistry = new DatabaseRegistry( 198 new DatabaseRegistry.Callback() { 199 @Override 200 public void onPostEvent(int databaseId, String path) { 201 dispatchDatabaseOpenedEvent(databaseId, path); 202 } 203 }, 204 new DatabaseRegistry.Callback() { 205 @Override 206 public void onPostEvent(int databaseId, String path) { 207 dispatchDatabaseClosedEvent(databaseId, path); 208 } 209 }); 210 211 mDatabaseLockRegistry = new DatabaseLockRegistry(); 212 } 213 214 @Override onReceiveCommand(byte @NonNull [] data, @NonNull CommandCallback callback)215 public void onReceiveCommand(byte @NonNull [] data, @NonNull CommandCallback callback) { 216 try { 217 Command command = Command.parseFrom(data); 218 switch (command.getOneOfCase()) { 219 case TRACK_DATABASES: 220 handleTrackDatabases(callback); 221 break; 222 case GET_SCHEMA: 223 handleGetSchema(command.getGetSchema(), callback); 224 break; 225 case QUERY: 226 handleQuery(command.getQuery(), callback); 227 break; 228 case KEEP_DATABASES_OPEN: 229 handleKeepDatabasesOpen(command.getKeepDatabasesOpen(), callback); 230 break; 231 case ACQUIRE_DATABASE_LOCK: 232 handleAcquireDatabaseLock(command.getAcquireDatabaseLock(), callback); 233 break; 234 case RELEASE_DATABASE_LOCK: 235 handleReleaseDatabaseLock(command.getReleaseDatabaseLock(), callback); 236 break; 237 default: 238 callback.reply( 239 createErrorOccurredResponse( 240 "Unrecognised command type: " + command.getOneOfCase().name(), 241 null, 242 true, 243 ERROR_UNRECOGNISED_COMMAND).toByteArray()); 244 } 245 } catch (Exception exception) { 246 callback.reply( 247 createErrorOccurredResponse( 248 "Unhandled Exception while processing the command: " 249 + exception.getMessage(), 250 stackTraceFromException(exception), 251 null, 252 ERROR_UNKNOWN).toByteArray() 253 ); 254 } 255 } 256 257 @Override onDispose()258 public void onDispose() { 259 super.onDispose(); 260 // TODO(161081452): release database locks and keep-open references 261 } 262 handleTrackDatabases(CommandCallback callback)263 private void handleTrackDatabases(CommandCallback callback) { 264 callback.reply(Response.newBuilder() 265 .setTrackDatabases(TrackDatabasesResponse.getDefaultInstance()) 266 .build().toByteArray() 267 ); 268 269 registerReleaseReferenceHooks(); 270 registerDatabaseOpenedHooks(); 271 272 EntryExitMatchingHookRegistry hookRegistry = new EntryExitMatchingHookRegistry( 273 mEnvironment); 274 275 registerInvalidationHooks(hookRegistry); 276 registerDatabaseClosedHooks(hookRegistry); 277 278 // Check for database instances in memory 279 for (SQLiteDatabase instance : 280 mEnvironment.artTooling().findInstances(SQLiteDatabase.class)) { 281 /* the race condition here will be handled by mDatabaseRegistry */ 282 if (instance.isOpen()) { 283 onDatabaseOpened(instance); 284 } else { 285 onDatabaseClosed(instance); 286 } 287 } 288 289 // Check for database instances on disk 290 for (Application instance : mEnvironment.artTooling().findInstances(Application.class)) { 291 for (String name : instance.databaseList()) { 292 File path = instance.getDatabasePath(name); 293 if (path.exists() && !isHelperSqliteFile(path)) { 294 mDatabaseRegistry.notifyOnDiskDatabase(path.getAbsolutePath()); 295 } 296 } 297 } 298 } 299 300 /** 301 * Secures a lock (transaction) on the database. Note that while the lock is in place, no 302 * changes to the database are possible: 303 * - the lock prevents other threads from modifying the database, 304 * - lock thread, on releasing the lock, rolls-back all changes (transaction is rolled-back). 305 */ 306 @SuppressWarnings("FutureReturnValueIgnored") // code inside the future is exception-proofed handleAcquireDatabaseLock( AcquireDatabaseLockCommand command, final CommandCallback callback)307 private void handleAcquireDatabaseLock( 308 AcquireDatabaseLockCommand command, 309 final CommandCallback callback) { 310 final int databaseId = command.getDatabaseId(); 311 final DatabaseConnection connection = acquireConnection(databaseId, callback); 312 if (connection == null) return; 313 314 // Timeout is covered by mDatabaseLockRegistry 315 SqliteInspectionExecutors.submit(mIOExecutor, new Runnable() { 316 @Override 317 public void run() { 318 int lockId; 319 try { 320 lockId = mDatabaseLockRegistry.acquireLock(databaseId, connection.mDatabase); 321 } catch (Exception e) { 322 processLockingException(callback, e, true); 323 return; 324 } 325 326 callback.reply(Response.newBuilder().setAcquireDatabaseLock( 327 AcquireDatabaseLockResponse.newBuilder().setLockId(lockId) 328 ).build().toByteArray()); 329 } 330 }); 331 } 332 333 @SuppressWarnings("FutureReturnValueIgnored") // code inside the future is exception-proofed handleReleaseDatabaseLock( final ReleaseDatabaseLockCommand command, final CommandCallback callback)334 private void handleReleaseDatabaseLock( 335 final ReleaseDatabaseLockCommand command, 336 final CommandCallback callback) { 337 // Timeout is covered by mDatabaseLockRegistry 338 SqliteInspectionExecutors.submit(mIOExecutor, new Runnable() { 339 @Override 340 public void run() { 341 try { 342 mDatabaseLockRegistry.releaseLock(command.getLockId()); 343 } catch (Exception e) { 344 processLockingException(callback, e, false); 345 return; 346 } 347 callback.reply(Response.newBuilder().setReleaseDatabaseLock( 348 ReleaseDatabaseLockResponse.getDefaultInstance() 349 ).build().toByteArray()); 350 } 351 }); 352 } 353 354 /** 355 * @param isLockingStage provide true for acquiring a lock; false for releasing a lock 356 */ processLockingException(CommandCallback callback, Exception exception, boolean isLockingStage)357 private void processLockingException(CommandCallback callback, Exception exception, 358 boolean isLockingStage) { 359 ErrorCode errorCode = ((exception instanceof IllegalStateException) 360 && isAttemptAtUsingClosedDatabase((IllegalStateException) exception)) 361 ? ERROR_DB_CLOSED_DURING_OPERATION 362 : ERROR_ISSUE_WITH_LOCKING_DATABASE; 363 364 String message = isLockingStage 365 ? "Issue while trying to lock the database for the export operation: " 366 : "Issue while trying to unlock the database after the export operation: "; 367 368 Boolean isRecoverable = isLockingStage 369 ? true // failure to lock the db should be recoverable 370 : null; // not sure if we can recover from a failure to unlock the db, so UNKNOWN 371 372 callback.reply(createErrorOccurredResponse(message, isRecoverable, exception, 373 errorCode).toByteArray()); 374 } 375 376 /** 377 * Tracking potential database closed events via 378 * {@link #ALL_REFERENCES_RELEASE_COMMAND_SIGNATURE} 379 */ registerDatabaseClosedHooks(EntryExitMatchingHookRegistry hookRegistry)380 private void registerDatabaseClosedHooks(EntryExitMatchingHookRegistry hookRegistry) { 381 hookRegistry.registerHook(SQLiteDatabase.class, ALL_REFERENCES_RELEASE_COMMAND_SIGNATURE, 382 new EntryExitMatchingHookRegistry.OnExitCallback() { 383 @Override 384 public void onExit(EntryExitMatchingHookRegistry.Frame exitFrame) { 385 final Object thisObject = exitFrame.mThisObject; 386 if (thisObject instanceof SQLiteDatabase) { 387 onDatabaseClosed((SQLiteDatabase) thisObject); 388 } 389 } 390 }); 391 } 392 registerDatabaseOpenedHooks()393 private void registerDatabaseOpenedHooks() { 394 List<String> methods = (Build.VERSION.SDK_INT < 27) 395 ? Arrays.asList(OPEN_DATABASE_COMMAND_SIGNATURE_API_11) 396 : Arrays.asList(OPEN_DATABASE_COMMAND_SIGNATURE_API_27, 397 OPEN_DATABASE_COMMAND_SIGNATURE_API_11, 398 CREATE_IN_MEMORY_DATABASE_COMMAND_SIGNATURE_API_27); 399 400 ExitHook<SQLiteDatabase> hook = 401 new ExitHook<SQLiteDatabase>() { 402 @Override 403 public SQLiteDatabase onExit(SQLiteDatabase database) { 404 try { 405 onDatabaseOpened(database); 406 } catch (Exception exception) { 407 getConnection().sendEvent(createErrorOccurredEvent( 408 "Unhandled Exception while processing an onDatabaseAdded " 409 + "event: " 410 + exception.getMessage(), 411 stackTraceFromException(exception), null, 412 ERROR_ISSUE_WITH_PROCESSING_NEW_DATABASE_CONNECTION) 413 .toByteArray()); 414 } 415 return database; 416 } 417 }; 418 for (String method : methods) { 419 mEnvironment.artTooling().registerExitHook(SQLiteDatabase.class, method, hook); 420 } 421 } 422 registerReleaseReferenceHooks()423 private void registerReleaseReferenceHooks() { 424 mEnvironment.artTooling().registerEntryHook( 425 SQLiteClosable.class, 426 "releaseReference()V", 427 new EntryHook() { 428 @Override 429 public void onEntry(@Nullable Object thisObject, 430 @NonNull List<Object> args) { 431 if (thisObject instanceof SQLiteDatabase) { 432 mDatabaseRegistry.notifyReleaseReference((SQLiteDatabase) thisObject); 433 } 434 } 435 }); 436 } 437 registerInvalidationHooks(EntryExitMatchingHookRegistry hookRegistry)438 private void registerInvalidationHooks(EntryExitMatchingHookRegistry hookRegistry) { 439 /* 440 * Schedules a task using {@link mScheduledExecutor} and executes it on {@link mIOExecutor}. 441 */ 442 final RequestCollapsingThrottler.DeferredExecutor deferredExecutor = 443 new RequestCollapsingThrottler.DeferredExecutor() { 444 @Override 445 @SuppressWarnings("FutureReturnValueIgnored") // TODO: handle errors from Future 446 public void schedule(final Runnable command, final long delayMs) { 447 mEnvironment.executors().handler().postDelayed(new Runnable() { 448 @Override 449 public void run() { 450 mIOExecutor.execute(command); 451 } 452 }, delayMs); 453 } 454 }; 455 final RequestCollapsingThrottler throttler = new RequestCollapsingThrottler( 456 INVALIDATION_MIN_INTERVAL_MS, 457 new Runnable() { 458 @Override 459 public void run() { 460 dispatchDatabasePossiblyChangedEvent(); 461 } 462 }, deferredExecutor); 463 464 registerInvalidationHooksSqliteStatement(throttler); 465 registerInvalidationHooksTransaction(throttler); 466 registerInvalidationHooksSQLiteCursor(throttler, hookRegistry); 467 } 468 469 /** 470 * Triggering invalidation on {@link SQLiteDatabase#endTransaction} allows us to avoid 471 * showing incorrect stale values that could originate from a mid-transaction query. 472 * 473 * TODO: track if transaction committed or rolled back by observing if 474 * {@link SQLiteDatabase#setTransactionSuccessful} was called 475 */ registerInvalidationHooksTransaction(final RequestCollapsingThrottler throttler)476 private void registerInvalidationHooksTransaction(final RequestCollapsingThrottler throttler) { 477 mEnvironment.artTooling().registerExitHook(SQLiteDatabase.class, "endTransaction()V", 478 new ExitHook<Object>() { 479 @Override 480 public Object onExit(Object result) { 481 throttler.submitRequest(); 482 return result; 483 } 484 }); 485 } 486 487 /** 488 * Invalidation hooks triggered by: 489 * <ul> 490 * <li>{@link SQLiteStatement#execute}</li> 491 * <li>{@link SQLiteStatement#executeInsert}</li> 492 * <li>{@link SQLiteStatement#executeUpdateDelete}</li> 493 * </ul> 494 */ registerInvalidationHooksSqliteStatement( final RequestCollapsingThrottler throttler)495 private void registerInvalidationHooksSqliteStatement( 496 final RequestCollapsingThrottler throttler) { 497 for (String method : SQLITE_STATEMENT_EXECUTE_METHODS_SIGNATURES) { 498 mEnvironment.artTooling().registerExitHook(SQLiteStatement.class, method, 499 new ExitHook<Object>() { 500 @Override 501 public Object onExit(Object result) { 502 throttler.submitRequest(); 503 return result; 504 } 505 }); 506 } 507 } 508 509 /** 510 * Invalidation hooks triggered by {@link SQLiteCursor#close()} 511 * which means that the cursor's query was executed. 512 * <p> 513 * In order to access cursor's query, we also use {@link SQLiteDatabase#rawQueryWithFactory} 514 * which takes a query String and constructs a cursor based on it. 515 */ registerInvalidationHooksSQLiteCursor(final RequestCollapsingThrottler throttler, EntryExitMatchingHookRegistry hookRegistry)516 private void registerInvalidationHooksSQLiteCursor(final RequestCollapsingThrottler throttler, 517 EntryExitMatchingHookRegistry hookRegistry) { 518 519 // TODO: add active pruning via Cursor#close listener 520 final Map<SQLiteCursor, Void> trackedCursors = Collections.synchronizedMap( 521 new WeakHashMap<SQLiteCursor, Void>()); 522 523 final String rawQueryMethodSignature = "rawQueryWithFactory(" 524 + "Landroid/database/sqlite/SQLiteDatabase$CursorFactory;" 525 + "Ljava/lang/String;" 526 + "[Ljava/lang/String;" 527 + "Ljava/lang/String;" 528 + "Landroid/os/CancellationSignal;" 529 + ")Landroid/database/Cursor;"; 530 hookRegistry.registerHook(SQLiteDatabase.class, 531 rawQueryMethodSignature, new EntryExitMatchingHookRegistry.OnExitCallback() { 532 @Override 533 public void onExit(EntryExitMatchingHookRegistry.Frame exitFrame) { 534 SQLiteCursor cursor = cursorParam(exitFrame.mResult); 535 String query = stringParam(exitFrame.mArgs.get(1)); 536 537 // Only track cursors that might modify the database. 538 // TODO: handle PRAGMA select queries, e.g. PRAGMA_TABLE_INFO 539 if (cursor != null && query != null && getSqlStatementType(query) 540 != DatabaseUtils.STATEMENT_SELECT) { 541 trackedCursors.put(cursor, null); 542 } 543 } 544 }); 545 546 547 mEnvironment.artTooling().registerEntryHook(SQLiteCursor.class, "close()V", 548 new ArtTooling.EntryHook() { 549 @Override 550 public void onEntry(@Nullable Object thisObject, @NonNull List<Object> args) { 551 if (trackedCursors.containsKey(thisObject)) { 552 throttler.submitRequest(); 553 } 554 } 555 }); 556 } 557 558 // Gets a SQLiteCursor from a passed-in Object (if possible) cursorParam(Object cursor)559 private @Nullable SQLiteCursor cursorParam(Object cursor) { 560 if (cursor instanceof SQLiteCursor) { 561 return (SQLiteCursor) cursor; 562 } 563 564 if (cursor instanceof CursorWrapper) { 565 CursorWrapper wrapper = (CursorWrapper) cursor; 566 return cursorParam(wrapper.getWrappedCursor()); 567 } 568 569 // TODO: add support for more cursor types 570 Log.w(SqliteInspector.class.getName(), String.format( 571 "Unsupported Cursor type: %s. Invalidation might not work correctly.", cursor)); 572 return null; 573 } 574 575 // Gets a String from a passed-in Object (if possible) stringParam(Object string)576 private @Nullable String stringParam(Object string) { 577 return string instanceof String ? (String) string : null; 578 } 579 dispatchDatabaseOpenedEvent(int databaseId, String path)580 private void dispatchDatabaseOpenedEvent(int databaseId, String path) { 581 getConnection().sendEvent(Event.newBuilder().setDatabaseOpened( 582 DatabaseOpenedEvent.newBuilder().setDatabaseId(databaseId).setPath(path) 583 ).build().toByteArray()); 584 } 585 dispatchDatabaseClosedEvent(int databaseId, String path)586 private void dispatchDatabaseClosedEvent(int databaseId, String path) { 587 getConnection().sendEvent(Event.newBuilder().setDatabaseClosed( 588 DatabaseClosedEvent.newBuilder().setDatabaseId(databaseId).setPath(path) 589 ).build().toByteArray()); 590 } 591 dispatchDatabasePossiblyChangedEvent()592 private void dispatchDatabasePossiblyChangedEvent() { 593 getConnection().sendEvent(Event.newBuilder().setDatabasePossiblyChanged( 594 DatabasePossiblyChangedEvent.getDefaultInstance() 595 ).build().toByteArray()); 596 } 597 598 @SuppressWarnings("FutureReturnValueIgnored") // code inside the future is exception-proofed handleGetSchema(GetSchemaCommand command, final CommandCallback callback)599 private void handleGetSchema(GetSchemaCommand command, final CommandCallback callback) { 600 final DatabaseConnection connection = acquireConnection(command.getDatabaseId(), callback); 601 if (connection == null) return; 602 603 // TODO: consider a timeout 604 SqliteInspectionExecutors.submit(connection.mExecutor, new Runnable() { 605 @Override 606 public void run() { 607 callback.reply(querySchema(connection.mDatabase).toByteArray()); 608 } 609 }); 610 } 611 handleQuery(final QueryCommand command, final CommandCallback callback)612 private void handleQuery(final QueryCommand command, final CommandCallback callback) { 613 final DatabaseConnection connection = acquireConnection(command.getDatabaseId(), callback); 614 if (connection == null) return; 615 616 final CancellationSignal cancellationSignal = new CancellationSignal(); 617 final Executor executor = connection.mExecutor; 618 // TODO: consider a timeout 619 final Future<?> future = SqliteInspectionExecutors.submit(executor, new Runnable() { 620 @Override 621 public void run() { 622 String[] params = parseQueryParameterValues(command); 623 Cursor cursor = null; 624 try { 625 cursor = rawQuery(connection.mDatabase, command.getQuery(), params, 626 cancellationSignal); 627 628 long responseSizeLimitHint = command.getResponseSizeLimitHint(); 629 // treating unset field as unbounded 630 if (responseSizeLimitHint <= 0) responseSizeLimitHint = Long.MAX_VALUE; 631 632 List<String> columnNames = Arrays.asList(cursor.getColumnNames()); 633 callback.reply(Response.newBuilder() 634 .setQuery(QueryResponse.newBuilder() 635 .addAllRows(convert(cursor, responseSizeLimitHint)) 636 .addAllColumnNames(columnNames) 637 .build()) 638 .build() 639 .toByteArray() 640 ); 641 triggerInvalidation(command.getQuery()); 642 } catch (SQLiteException | IllegalArgumentException e) { 643 callback.reply(createErrorOccurredResponse(e, true, 644 ERROR_ISSUE_WITH_PROCESSING_QUERY).toByteArray()); 645 } catch (IllegalStateException e) { 646 if (isAttemptAtUsingClosedDatabase(e)) { 647 callback.reply(createErrorOccurredResponse(e, true, 648 ERROR_DB_CLOSED_DURING_OPERATION).toByteArray()); 649 } else { 650 callback.reply(createErrorOccurredResponse(e, null, 651 ERROR_UNKNOWN).toByteArray()); 652 } 653 } catch (Exception e) { 654 callback.reply(createErrorOccurredResponse(e, null, 655 ERROR_UNKNOWN).toByteArray()); 656 } finally { 657 if (cursor != null) { 658 cursor.close(); 659 } 660 } 661 } 662 }); 663 callback.addCancellationListener(mEnvironment.executors().primary(), new Runnable() { 664 @Override 665 public void run() { 666 cancellationSignal.cancel(); 667 future.cancel(true); 668 } 669 }); 670 } 671 triggerInvalidation(String query)672 private void triggerInvalidation(String query) { 673 if (getSqlStatementType(query) != DatabaseUtils.STATEMENT_SELECT) { 674 for (Invalidation invalidation : mInvalidations) { 675 invalidation.triggerInvalidations(); 676 } 677 } 678 } 679 handleKeepDatabasesOpen(KeepDatabasesOpenCommand keepDatabasesOpen, CommandCallback callback)680 private void handleKeepDatabasesOpen(KeepDatabasesOpenCommand keepDatabasesOpen, 681 CommandCallback callback) { 682 // Acknowledge the command 683 callback.reply(Response.newBuilder().setKeepDatabasesOpen( 684 KeepDatabasesOpenResponse.getDefaultInstance() 685 ).build().toByteArray()); 686 687 mDatabaseRegistry.notifyKeepOpenToggle(keepDatabasesOpen.getSetEnabled()); 688 } 689 690 @SuppressLint("Recycle") // For: "The cursor should be freed up after use with #close" rawQuery(@onNull SQLiteDatabase database, @NonNull String queryText, final String @NonNull [] params, @Nullable CancellationSignal cancellationSignal)691 private static Cursor rawQuery(@NonNull SQLiteDatabase database, @NonNull String queryText, 692 final String @NonNull [] params, @Nullable CancellationSignal cancellationSignal) { 693 SQLiteDatabase.CursorFactory cursorFactory = new SQLiteDatabase.CursorFactory() { 694 @Override 695 public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver driver, 696 String editTable, SQLiteQuery query) { 697 for (int i = 0; i < params.length; i++) { 698 String value = params[i]; 699 int index = i + 1; 700 if (value == null) { 701 query.bindNull(index); 702 } else { 703 query.bindString(index, value); 704 } 705 } 706 return new SQLiteCursor(driver, editTable, query); 707 } 708 }; 709 710 return database.rawQueryWithFactory(cursorFactory, queryText, null, null, 711 cancellationSignal); 712 } 713 parseQueryParameterValues(QueryCommand command)714 private static String @NonNull [] parseQueryParameterValues(QueryCommand command) { 715 String[] params = new String[command.getQueryParameterValuesCount()]; 716 for (int i = 0; i < command.getQueryParameterValuesCount(); i++) { 717 QueryParameterValue param = command.getQueryParameterValues(i); 718 switch (param.getOneOfCase()) { 719 case STRING_VALUE: 720 params[i] = param.getStringValue(); 721 break; 722 case ONEOF_NOT_SET: 723 params[i] = null; 724 break; 725 default: 726 throw new IllegalArgumentException( 727 "Unsupported parameter type. OneOfCase=" + param.getOneOfCase()); 728 } 729 } 730 return params; 731 } 732 733 /** 734 * Tries to find a database for an id. If no such database is found, it replies with an 735 * {@link ErrorOccurredResponse} via the {@code callback} provided. 736 * 737 * TODO: remove race condition (affects WAL=off) 738 * - lock request is received and in the process of being secured 739 * - query request is received and since no lock in place, receives an IO Executor 740 * - lock request completes and holds a lock on the database 741 * - query cannot run because there is a lock in place 742 * 743 * The race condition can be mitigated by clients by securing a lock synchronously with no 744 * other queries in place. 745 * 746 * @return null if no database found for the provided id. A database reference otherwise. 747 */ acquireConnection(int databaseId, CommandCallback callback)748 private @Nullable DatabaseConnection acquireConnection(int databaseId, 749 CommandCallback callback) { 750 DatabaseConnection connection = mDatabaseLockRegistry.getConnection(databaseId); 751 if (connection != null) { 752 // With WAL enabled, we prefer to use the IO executor. With WAL off we don't have a 753 // choice and must use the executor that has a lock (transaction) on the database. 754 return connection.mDatabase.isWriteAheadLoggingEnabled() 755 ? new DatabaseConnection(connection.mDatabase, mIOExecutor) 756 : connection; 757 } 758 759 SQLiteDatabase database = mDatabaseRegistry.getConnection(databaseId); 760 if (database == null) { 761 replyNoDatabaseWithId(callback, databaseId); 762 return null; 763 } 764 765 // Given no lock, IO executor is appropriate. 766 return new DatabaseConnection(database, mIOExecutor); 767 } 768 769 /** 770 * @param responseSizeLimitHint expressed in bytes 771 */ convert(Cursor cursor, long responseSizeLimitHint)772 private static List<Row> convert(Cursor cursor, long responseSizeLimitHint) { 773 long responseSize = 0; 774 List<Row> result = new ArrayList<>(); 775 int columnCount = cursor.getColumnCount(); 776 while (cursor.moveToNext() && responseSize < responseSizeLimitHint) { 777 Row.Builder rowBuilder = Row.newBuilder(); 778 for (int i = 0; i < columnCount; i++) { 779 CellValue value = readValue(cursor, i); 780 rowBuilder.addValues(value); 781 } 782 Row row = rowBuilder.build(); 783 // Optimistically adding a row before checking the limit. Eliminates the case when a 784 // misconfigured client (limit too low) is unable to fetch any results. Row size in 785 // SQLite Android is limited to (~2MB), so the worst case scenario is very manageable. 786 result.add(row); 787 responseSize += row.getSerializedSize(); 788 } 789 return result; 790 } 791 readValue(Cursor cursor, int index)792 private static CellValue readValue(Cursor cursor, int index) { 793 CellValue.Builder builder = CellValue.newBuilder(); 794 795 switch (cursor.getType(index)) { 796 case Cursor.FIELD_TYPE_NULL: 797 // no field to set 798 break; 799 case Cursor.FIELD_TYPE_BLOB: 800 builder.setBlobValue(ByteString.copyFrom(cursor.getBlob(index))); 801 break; 802 case Cursor.FIELD_TYPE_STRING: 803 builder.setStringValue(cursor.getString(index)); 804 break; 805 case Cursor.FIELD_TYPE_INTEGER: 806 builder.setLongValue(cursor.getLong(index)); 807 break; 808 case Cursor.FIELD_TYPE_FLOAT: 809 builder.setDoubleValue(cursor.getDouble(index)); 810 break; 811 } 812 813 return builder.build(); 814 } 815 replyNoDatabaseWithId(CommandCallback callback, int databaseId)816 private void replyNoDatabaseWithId(CommandCallback callback, int databaseId) { 817 String message = String.format("Unable to perform an operation on database (id=%s)." 818 + " The database may have already been closed.", databaseId); 819 callback.reply(createErrorOccurredResponse(message, null, true, 820 ERROR_NO_OPEN_DATABASE_WITH_REQUESTED_ID).toByteArray()); 821 } 822 querySchema(SQLiteDatabase database)823 private @NonNull Response querySchema(SQLiteDatabase database) { 824 Cursor cursor = null; 825 try { 826 cursor = rawQuery(database, sQueryTableInfo, new String[0], null); 827 GetSchemaResponse.Builder schemaBuilder = GetSchemaResponse.newBuilder(); 828 829 int objectTypeIx = cursor.getColumnIndex("type"); // view or table 830 int tableNameIx = cursor.getColumnIndex("tableName"); 831 int columnNameIx = cursor.getColumnIndex("columnName"); 832 int typeIx = cursor.getColumnIndex("columnType"); 833 int pkIx = cursor.getColumnIndex("pk"); 834 int notNullIx = cursor.getColumnIndex("notnull"); 835 int uniqueIx = cursor.getColumnIndex("unique"); 836 837 Table.Builder tableBuilder = null; 838 while (cursor.moveToNext()) { 839 String tableName = cursor.getString(tableNameIx); 840 841 // ignore certain tables 842 if (sHiddenTables.contains(tableName)) { 843 continue; 844 } 845 846 // check if getting data for a new table or appending columns to the current one 847 if (tableBuilder == null || !tableBuilder.getName().equals(tableName)) { 848 if (tableBuilder != null) { 849 schemaBuilder.addTables(tableBuilder.build()); 850 } 851 tableBuilder = Table.newBuilder(); 852 tableBuilder.setName(tableName); 853 tableBuilder.setIsView("view".equalsIgnoreCase(cursor.getString(objectTypeIx))); 854 } 855 856 // append column information to the current table info 857 tableBuilder.addColumns(Column.newBuilder() 858 .setName(cursor.getString(columnNameIx)) 859 .setType(cursor.getString(typeIx)) 860 .setPrimaryKey(cursor.getInt(pkIx)) 861 .setIsNotNull(cursor.getInt(notNullIx) > 0) 862 .setIsUnique(cursor.getInt(uniqueIx) > 0) 863 .build() 864 ); 865 } 866 if (tableBuilder != null) { 867 schemaBuilder.addTables(tableBuilder.build()); 868 } 869 870 return Response.newBuilder().setGetSchema(schemaBuilder.build()).build(); 871 } catch (IllegalStateException e) { 872 if (isAttemptAtUsingClosedDatabase(e)) { 873 return createErrorOccurredResponse(e, true, 874 ERROR_DB_CLOSED_DURING_OPERATION); 875 } else { 876 return createErrorOccurredResponse(e, null, 877 ERROR_UNKNOWN); 878 } 879 } catch (Exception e) { 880 return createErrorOccurredResponse(e, null, 881 ERROR_UNKNOWN); 882 } finally { 883 if (cursor != null) { 884 cursor.close(); 885 } 886 } 887 } 888 889 @SuppressWarnings("WeakerAccess") // avoiding a synthetic accessor onDatabaseOpened(SQLiteDatabase database)890 void onDatabaseOpened(SQLiteDatabase database) { 891 mRoomInvalidationRegistry.invalidateCache(); 892 mDatabaseRegistry.notifyDatabaseOpened(database); 893 } 894 895 @SuppressWarnings("WeakerAccess") // avoiding a synthetic accessor onDatabaseClosed(SQLiteDatabase database)896 void onDatabaseClosed(SQLiteDatabase database) { 897 mDatabaseRegistry.notifyAllDatabaseReferencesReleased(database); 898 } 899 createErrorOccurredEvent(@ullable String message, @Nullable String stackTrace, Boolean isRecoverable, ErrorCode errorCode)900 private Event createErrorOccurredEvent(@Nullable String message, @Nullable String stackTrace, 901 Boolean isRecoverable, ErrorCode errorCode) { 902 return Event.newBuilder().setErrorOccurred( 903 ErrorOccurredEvent.newBuilder() 904 .setContent( 905 createErrorContentMessage(message, 906 stackTrace, 907 isRecoverable, 908 errorCode)) 909 .build()) 910 .build(); 911 } 912 createErrorContentMessage(@ullable String message, @Nullable String stackTrace, Boolean isRecoverable, ErrorCode errorCode)913 private static ErrorContent createErrorContentMessage(@Nullable String message, 914 @Nullable String stackTrace, Boolean isRecoverable, ErrorCode errorCode) { 915 ErrorContent.Builder builder = ErrorContent.newBuilder(); 916 if (message != null) { 917 builder.setMessage(message); 918 } 919 if (stackTrace != null) { 920 builder.setStackTrace(stackTrace); 921 } 922 ErrorRecoverability.Builder recoverability = ErrorRecoverability.newBuilder(); 923 if (isRecoverable != null) { // leave unset otherwise, which translates to 'unknown' 924 recoverability.setIsRecoverable(isRecoverable); 925 } 926 builder.setRecoverability(recoverability.build()); 927 builder.setErrorCode(errorCode); 928 return builder.build(); 929 } 930 createErrorOccurredResponse(@onNull Exception exception, Boolean isRecoverable, ErrorCode errorCode)931 private static Response createErrorOccurredResponse(@NonNull Exception exception, 932 Boolean isRecoverable, ErrorCode errorCode) { 933 return createErrorOccurredResponse("", isRecoverable, exception, errorCode); 934 } 935 createErrorOccurredResponse(@onNull String messagePrefix, Boolean isRecoverable, @NonNull Exception exception, ErrorCode errorCode)936 private static Response createErrorOccurredResponse(@NonNull String messagePrefix, 937 Boolean isRecoverable, @NonNull Exception exception, ErrorCode errorCode) { 938 String message = exception.getMessage(); 939 if (message == null) message = exception.toString(); 940 return createErrorOccurredResponse(messagePrefix + message, 941 stackTraceFromException(exception), isRecoverable, errorCode); 942 } 943 createErrorOccurredResponse(@ullable String message, @Nullable String stackTrace, Boolean isRecoverable, ErrorCode errorCode)944 private static Response createErrorOccurredResponse(@Nullable String message, 945 @Nullable String stackTrace, Boolean isRecoverable, ErrorCode errorCode) { 946 return Response.newBuilder() 947 .setErrorOccurred( 948 ErrorOccurredResponse.newBuilder() 949 .setContent(createErrorContentMessage(message, stackTrace, 950 isRecoverable, errorCode))) 951 .build(); 952 } 953 stackTraceFromException(Exception exception)954 private static @NonNull String stackTraceFromException(Exception exception) { 955 StringWriter writer = new StringWriter(); 956 exception.printStackTrace(new PrintWriter(writer)); 957 return writer.toString(); 958 } 959 isHelperSqliteFile(File file)960 private static boolean isHelperSqliteFile(File file) { 961 String path = file.getPath(); 962 return path.endsWith("-journal") || path.endsWith("-shm") || path.endsWith("-wal"); 963 } 964 965 /** 966 * Provides a reference to the database and an executor to access the database. 967 * 968 * Executor is relevant in the context of locking, where a locked database with WAL disabled 969 * needs to run queries on the thread that locked it. 970 */ 971 static final class DatabaseConnection { 972 final @NonNull SQLiteDatabase mDatabase; 973 final @NonNull Executor mExecutor; 974 DatabaseConnection(@onNull SQLiteDatabase database, @NonNull Executor executor)975 DatabaseConnection(@NonNull SQLiteDatabase database, @NonNull Executor executor) { 976 mDatabase = database; 977 mExecutor = executor; 978 } 979 } 980 } 981