andpor / react-native-sqlite-storage

Full featured SQLite3 Native Plugin for React Native (Android and iOS)
MIT License
2.75k stars 521 forks source link

iOS: How to deal with App Extensions? How to open a db from a shared group? #308

Open pcboy opened 5 years ago

pcboy commented 5 years ago

I'm trying to open a db from a path looking like:

/private/var/mobile/Containers/Shared/AppGroup/F4CECCE9-569A-4AB5-B0D7-1C7E705295F3/default.sqlite

Both my app and my call directory extension are using the same app group. So I want the database to be in there.
Problem is SQLite.openDatabase doesn't seem to take an absolute path. There is always a lot of logic involved to find the proper path. Am I doing something wrong? (not an iOS dev)

jordoh commented 5 years ago

Hi @pcboy - I've run into the same issue, as there doesn't appear to be any way to use an arbitrary location for the database. If you happen to still need to do this, you can patch in a hardcoded "Shared" location option pretty easily (replacing YOUR_SHARED_GROUP_NAME with your group container name):

--- a/node_modules/react-native-sqlite-storage/lib/sqlite.core.js
+++ b/node_modules/react-native-sqlite-storage/lib/sqlite.core.js
@@ -707,7 +707,8 @@ SQLitePluginTransaction.prototype.abortFromQ = function(sqlerror) {
 dblocations = {
   'default' : 'nosync',
   'Documents' : 'docs',
-  'Library' : 'libs'
+  'Library' : 'libs',
+  'Shared' : 'shared'
 };

 SQLiteFactory = function(){};
--- a/node_modules/react-native-sqlite-storage/src/ios/SQLite.m
+++ b/node_modules/react-native-sqlite-storage/src/ios/SQLite.m
@@ -119,6 +119,13 @@ - (id) init
         [appDBPaths setObject: libs forKey:@"nosync"];
       }
     }
+
+    NSURL* groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"YOUR_SHARED_GROUP_NAME"];
+    if (groupURL != NULL) {
+       NSString* shared = groupURL.path;
+       RCTLog(@"Detected Shared path: %@", shared);
+       [appDBPaths setObject: shared forKey:@"shared"];
+    }
   }
   return self;
 }
@@ -226,6 +233,9 @@ -(id) getDBPath:(NSString *)dbFile at:(NSString *)atkey {
             }
           }
 #endif
+
+          sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
+
           // Attempt to read the SQLite master table [to support SQLCipher version]:
           if(sqlite3_exec(db, (const char*)"SELECT count(*) FROM sqlite_master;", NULL, NULL, NULL) == SQLITE_OK) {
             NSValue *dbPointer = [NSValue valueWithPointer:db];

then you can open the database as:

SQLite.openDatabase({ name: "something.db", location: "Shared" })
poltak commented 5 years ago

Thanks for those patches @jordoh. It seems to work well for the app we're developing here too. It would be great to submit a PR to try and get this supported in the package on NPM. I have forked off and was intending to do that myself, however soon realized that I have no idea how to make the group ID not hardcoded in the native code - my experience with the native platforms are so far mostly non-existent.

Is this something you (or anyone else with a bit more understanding) would be interested in doing, or willing to guide me a bit in doing?

jordoh commented 5 years ago

Hi @poltak - I don't have the cycles at the moment, but it should be relatively straightforward to make it configurable. There are a couple ways that come to mind:

  1. Pass the app group name through to the library. This one is a bit tricky because SQLite.m sets up a mapping of location names (e.g. "shared ") to paths on init and the JS code just references those location names. You could pass the app group name through to open and delete so it can be passed to getDBPath and used dynamically, then expose an additional option in the JS interface.

  2. A simpler approach, building off the patch above, would be adding an "AppGroupName" key to your Info.plist, which could then be read prior to calling containerURLForSecurityApplicationGroupIdentifier with:

    [[NSBundle mainBundle] objectForInfoDictionaryKey:@"AppGroupName"];
jordoh commented 5 years ago

I've updated the patch above to include a subtle difference: switching the database to WAL journaling mode with: sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);

This is necessary due to an obscure iOS behavior where processes will be terminated if they are backgrounded while holding a lock on a file in a shared container, unless the file is a SQLite database in WAL mode (presumably Apple is using such databases themselves, hence the exception, which appears to be tied to reading the header of the SQLite database to check for WAL mode).

Without WAL mode, you'll see crashes in the wild if the app is backgrounded while the database is being modified, e.g.:

Exception Type:  EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Termination Reason: Namespace SPRINGBOARD, Code 0xdead10cc
Termination Description: SPRINGBOARD, com.yourapp was task-suspended with locked system files: | /var/mobile/Containers/Shared/AppGroup/****.db | ProcessVisibility: Background | ProcessState: Suspended

with a thread writing to the database:

Thread 14 name:
Thread 14:
0   libsystem_kernel.dylib          0x00000001b2e78408 fsync + 8
1   libsqlite3.dylib                0x00000001b373e3ec unixSync + 268 (sqlite3.c:37614)
2   libsqlite3.dylib                0x00000001b3748e2c syncJournal + 516 (sqlite3.c:21998)
3   libsqlite3.dylib                0x00000001b373dcf8 sqlite3PagerCommitPhaseOne + 1288 (sqlite3.c:62699)
4   libsqlite3.dylib                0x00000001b3728154 sqlite3BtreeCommitPhaseOne + 164 (sqlite3.c:72229)
5   libsqlite3.dylib                0x00000001b36f3df4 sqlite3VdbeHalt + 2784 (sqlite3.c:83547)
6   libsqlite3.dylib                0x00000001b3720b00 sqlite3VdbeExec + 59944 (sqlite3.c:89508)
7   libsqlite3.dylib                0x00000001b3710b40 sqlite3_step + 444 (sqlite3.c:86851)
8   yourapp                         0x0000000102e75d4c -[SQLite executeSqlWithDict:andArgs:] + 1492 (SQLite.m:516)
9   yourapp                         0x0000000102e750e4 -[SQLite executeSqlBatch:success:error:] + 432 (SQLite.m:419)
10  libdispatch.dylib               0x00000001b2d18a38 _dispatch_call_block_and_release + 24 (init.c:1372)
11  libdispatch.dylib               0x00000001b2d197d4 _dispatch_client_callout + 16 (object.m:511)
12  libdispatch.dylib               0x00000001b2cbdc80 _dispatch_queue_override_invoke + 684 (inline_internal.h:2441)
13  libdispatch.dylib               0x00000001b2cca030 _dispatch_root_queue_drain + 372 (inline_internal.h:2482)
14  libdispatch.dylib               0x00000001b2cca8d4 _dispatch_worker_thread2 + 128 (queue.c:6072)
15  libsystem_pthread.dylib         0x00000001b2efa1b4 _pthread_wqthread + 464 (pthread.c:2361)
16  libsystem_pthread.dylib         0x00000001b2efccd4 start_wqthread + 4
poltak commented 5 years ago

Thanks for your help @jordoh. I successfully implemented your 2nd approach in https://github.com/WorldBrain/react-native-sqlite-storage/commit/7d6c2fc64f5f48910a33e28dff24b0f286581b2e

JeanEXE commented 4 years ago

@jordoh thanks!!
Has a possibility to add this solution at source code from a pull request??

jordoh commented 4 years ago

HI @LulinhaReparador - @poltak has an open PR for this in https://github.com/andpor/react-native-sqlite-storage/pull/374.

ssshake commented 4 years ago

Hey there, could it be possible to allow me to just specify the full path as a string? I already have the shared path in my application for other reasons so it would be convenient if I could simply just supply the string that I already have.