Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue 1111 only initialize Realm and KeyChainService once #1117

Merged
merged 30 commits into from
Dec 1, 2021

Conversation

tomholub
Copy link
Collaborator

This PR cleans up realm init to minimize race conditions and have more control over when and how it's initialized.

close #1111

WIP


  • Does not need tests (refactor only, docs or internal changes)

To be filled by reviewers

I have reviewed that this PR... (tick whichever items you personally focused on during this review):

  • addresses the issue it closes (if any)
  • code is readable and understandable
  • is accompanied with tests, or tests are not needed
  • is free of vulnerabilities
  • is documented clearly and usefully, or doesn't need documentation

@tomholub tomholub changed the title [wip] issue 1111 only initialize Realm and KeyChainService once issue 1111 only initialize Realm and KeyChainService once Nov 30, 2021
@tomholub
Copy link
Collaborator Author

Sorry for another monstrous PR. The main goal of this PR was:

  • initialize Realm only once, and in a controlled manner
  • make the initialization a MainActor
  • pass initialized storage to places that need it (which turned out to be every single place of the app 😞 ) through dependency injection

For this I created AppContext that gets passed around.

While I was doing it, certain things kept getting in my way, in particular magical currentUser usage hidden inside classes/methods, which needed DataProvider which needed EncryptedStorage which needed that initialized Realm.. it was a mess, and so I also did some more work:

  • where the above situation was particularly upsetting, I've instead added currentUser as method or class parameter
  • I've done some prepwork for current user should be dependency-injected #863 by requiring user to be present early in VC init. Next step, for that issue, would be to pass User into each VC init that requires it.

While doing this, I noticed some classes are used across the app and rely on storage, and so I've added them into AppContext as well. Which has gotten bigger than I expected.

Things that bother me about this PR and will need to get cleaned up (hopefully in other PRs):

  • the dependency injection highlights an overall mess present across the app. There are too many services, providers, often named wrongly, often their constructor properties are named wrongly, etc. It was more hidden until now when you could just put dataProvider = DataProvider() in every constructor, but now that it's truly dependency-injected, it really badly brings out some ugliness that we need to work on
  • repetitive code. I have written some code sequences 5+times again and again while refactoring this, and it would need a second look
  • general verbosity. similar as point above, and needs to be improved
  • inconsitency. Sometimes it is user, sometimes activeUser, sometimes currentUser.. some abstractions are somewhat different on IMAP and Gmail, and need to be alighned. etc

As of now, the app builds and runs and I didn't run into a bug yet. But Swift tests are likely broken. I'll have a look.

Copy link
Collaborator Author

@tomholub tomholub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some highlights below

FlowCrypt/App/AppContext.swift Outdated Show resolved Hide resolved
Comment on lines 14 to 22
class CheckMailAuthViewController: TableNodeViewController {
private let globalRouter: GlobalRouterType
private let appContext: AppContext

init(globalRouter: GlobalRouterType = GlobalRouter()) {
init(appContext: AppContext, globalRouter: GlobalRouterType = GlobalRouter()) {
self.appContext = appContext
self.globalRouter = globalRouter
super.init(node: TableNode())
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context now gets passed as first argument of every VC init.

Next step will be to pass User (not optional) into every VC that requires it. #863

Comment on lines 79 to 82
self.foldersProvider = foldersProvider ?? FoldersService(
encryptedStorage: appContext.encryptedStorage,
remoteFoldersProvider: appContext.getRequiredMailProvider().remoteFoldersProvider
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initialized FolderService many times this way, good candidate to go into AppContext (a method, maybe, similar to EmailProvider)

Comment on lines -84 to 85
private init(
currentAuthType: @autoclosure @escaping () -> (AuthType?),
services: [MailServiceProvider]
init(
currentAuthType: AuthType,
currentUser: User
) {
self.currentAuthType = currentAuthType
self.services = services
self.services = MailServiceProviderFactory.services(user: currentUser)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit worried about this - originally dynamic as a closure, now user is directly provided. Need to scrutinize more. But app seems to work the same.

@ivan-ushakov
Copy link
Contributor

Am I right that we have now one global context with app lifetime? What about Realm and threading? I see that context is forced to be created on UI thread, so Realms is created on that thread. That means we must do any read query on UI thread.

@tomholub
Copy link
Collaborator Author

tomholub commented Nov 30, 2021

Am I right that we have now one global context with app lifetime?

I believe so. (except that new AppContext gets created when session information changes - see AppContext.withSession but all other contents get copied over, including Realm / storage etc)

What about Realm and threading? I see that context is forced to be created on UI thread, so Realms is created on that thread. That means we must do any read query on UI thread.

That can be reworked - I don't mind to create Realm on any thread.

But the encryption key for realm, which uses KeyChain, must be created on main thread, to be safe.

I don't exactly mind sticking to main thread for Realm either, though. (if it doesn't cause issues, not sure)

@tomholub
Copy link
Collaborator Author

@ivan-ushakov could you please help me with these when running tests. I'm not very familiar with this stack.

image

@ivan-ushakov
Copy link
Contributor

@ivan-ushakov could you please help me with these when running tests. I'm not very familiar with this stack.

Sure, let me check.

@tomholub
Copy link
Collaborator Author

I'm still hoarding some changes here. I'll go ahead and push them

@ivan-ushakov ivan-ushakov marked this pull request as ready for review November 30, 2021 20:04
@ivan-ushakov ivan-ushakov marked this pull request as draft November 30, 2021 20:04
@@ -9,16 +9,9 @@
import Foundation
import UIKit

autoreleasepool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we could skip autoreleasepool here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either - the IDE was telling me that it's no longer needed. @sosnovsky may know

This comment was marked as resolved.

@ivan-ushakov
Copy link
Contributor

ivan-ushakov commented Nov 30, 2021

I'm still hoarding some changes here. I'll go ahead and push them

Looks like you added some UI classes to unit test target FlowCryptAppTests. We don't compile UI classes for unit tests so we don't add some Pod dependencies. I guess that this is the reason.

You could check Target Membership on the right side panel and remove checkmark for FlowCryptAppTests on UI files.

@ivan-ushakov
Copy link
Contributor

@ivan-ushakov I managed to break it again. Although this time there aren't so many files in test target.

What steps did you take to fix this last time? So that I can learn it too.

  1. git checkout master FlowCrypt.xcodeproj/project.pbxproj
  2. Fix several errors like missing files, links to removed files. Xcode will help here.

@sosnovsky
Copy link
Collaborator

image

I'd still like to be using custom arguments in the future - for appium tests mocks

These arguments can be parsed in AppDelegate using CommandLine.arguments array, no need for custom main.swift

@tomholub
Copy link
Collaborator Author

tomholub commented Dec 1, 2021

These arguments can be parsed in AppDelegate using CommandLine.arguments array, no need for custom main.swift

Got it! Filed #1130

@sosnovsky
Copy link
Collaborator

@ivan-ushakov I managed to break it again. Although this time there aren't so many files in test target.

What steps did you take to fix this last time? So that I can learn it too.

image

Current issues are caused by https://github.com/FlowCrypt/flowcrypt-ios/blob/issue-1111-realm-keychain-init/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift#L31 - this code needs CoreHost which imports SwiftyRSA framework, but it's not included in test target.

@sosnovsky
Copy link
Collaborator

I tried to remove CoreHost there, but then I got error about missing CoreHost in Core.swift file, trying to fix it now

@tomholub
Copy link
Collaborator Author

tomholub commented Dec 1, 2021

The Core parts are needed for tests in either case. And so is SwiftyRSA.

@tomholub
Copy link
Collaborator Author

tomholub commented Dec 1, 2021

Thanks for help!

@sosnovsky
Copy link
Collaborator

Also for testing we can use in-memory Realm instance, instead of on-disk - it should be faster and doesn't need to be resetted, as it's new instance for each new test (https://docs.mongodb.com/realm/sdk/ios/test-and-debug/#testing)

@tomholub
Copy link
Collaborator Author

tomholub commented Dec 1, 2021

I'm not entirely sure we use Realm in Swift tests. But will file an issue.

@sosnovsky
Copy link
Collaborator

The Core parts are needed for tests in either case. And so is SwiftyRSA.

Yes, this Core.swift code is 1 month old and previously worked well, so it's strange why it doesn't run after the latest updates.

@sosnovsky
Copy link
Collaborator

I'm not entirely sure we use Realm in Swift tests. But will file an issue.

It's initiated only in ComposeMessageServiceTests - https://github.com/FlowCrypt/flowcrypt-ios/blob/issue-1111-realm-keychain-init/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift#L36
But maybe later we'll need it in other places too

@sosnovsky
Copy link
Collaborator

Currently unit tests are running, but ComposeMessageServiceTests fail, probably because of some Realm issues in test environment (as previously ComposeMessageService didn't have encryptedStorage in init)

@tomholub
Copy link
Collaborator Author

tomholub commented Dec 1, 2021

Currently unit tests are running, but ComposeMessageServiceTests fail, probably because of some Realm issues in test environment (as previously ComposeMessageService didn't have encryptedStorage in init)

Yes but one of its dependencies must have previously still needed Realm, even if Realm wasn't touched in the tests.

Do you want to look into this? I'm ok to temporarily disable those tests, too.

@sosnovsky
Copy link
Collaborator

Currently unit tests are running, but ComposeMessageServiceTests fail, probably because of some Realm issues in test environment (as previously ComposeMessageService didn't have encryptedStorage in init)

Yes but one of its dependencies must have previously still needed Realm, even if Realm wasn't touched in the tests.

Do you want to look into this? I'm ok to temporarily disable those tests, too.

Currently looking into this issue

@sosnovsky
Copy link
Collaborator

All tests are passing now ✅

@sosnovsky sosnovsky self-requested a review December 1, 2021 15:07
@tomholub tomholub marked this pull request as ready for review December 1, 2021 15:11
@tomholub
Copy link
Collaborator Author

tomholub commented Dec 1, 2021

I'll merge this. After that, I'll review the already merged PR - there are many opportunities for small fixes. Then can settle that after. Thanks!

@tomholub tomholub merged commit 4162138 into master Dec 1, 2021
@tomholub tomholub deleted the issue-1111-realm-keychain-init branch December 1, 2021 15:12
Copy link
Collaborator Author

@tomholub tomholub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Below are smaller cleanup/refactors that I wanted to do in this PR

fetchedEncryptedKeys: [KeyDetails],
router: GlobalRouterType = GlobalRouter(),
keyStorage: KeyStorageType = KeyDataStorage(),
decorator: SetupViewDecorator = SetupViewDecorator(),
core: Core = Core.shared,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controllers don't need core in init, since we're not unit testing controllers

@@ -44,23 +43,21 @@ final class SetupBackupsViewController: TableNodeViewController, PassPhraseSavea
}

init(
appContext: AppContext,
fetchedEncryptedKeys: [KeyDetails],
router: GlobalRouterType = GlobalRouter(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Router not needed here, can get from context

Comment on lines -28 to 31
let core: Core
let core: Core = Core.shared
let router: GlobalRouterType
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core and router not needed here in init

user: UserId,
keys: [KeyDetails] = [],
core: Core = .shared,
router: GlobalRouterType = GlobalRouter(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

router not needed, get from ctx

user: UserId,
backupService: BackupServiceType = BackupService(),
core: Core = .shared,
router: GlobalRouterType = GlobalRouter(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can get router from ctx instead

Comment on lines +15 to +17
// todo - rename argument to folderService:
user: User,
folderProvider: FoldersServiceType,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename folderProvider to folderService here

logger.logInfo("proceed for session \(session.debugDescription)")
AppStartup().initializeApp(window: keyWindow, session: session)
private func proceed(with appContext: AppContext) {
logger.logInfo("proceed for session: \(appContext.session?.description ?? "nil")")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

originally it used debugDescription, not sure of the difference

Comment on lines +12 to 13
// todo - what is this and what is it used for?
final class KeyDataStorage {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this class, it just wraps functionality with no value add

storage: KeyStorageType = KeyDataStorage(),
passPhraseService: PassPhraseServiceType = PassPhraseService(),
currentUserEmail: @autoclosure @escaping () -> (String?) = DataService.shared.email
storage: KeyStorageType,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rename storage to keyStorage here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

realm crash: Realm file decryption failed
3 participants