firebase / firebase-unity-sdk

The Firebase SDK for Unity
http://firebase.google.com
Apache License 2.0
234 stars 38 forks source link

[Bug] Windows Crashes after exactly 1 hour when signed in to Firebase #540

Open megavoid opened 2 years ago

megavoid commented 2 years ago

[REQUIRED] Please fill in the following fields:

[REQUIRED] Please describe the issue here:

On Windows our software crashes exactly 1 hour after signing in to Firebase. This happens about 90% of the time on several test machines with the compiled App (both IL2CPP and Mono) and both with username/password login and resume session by token. Running content / scenes do not matter, it even crashes only being idle in main menu.

It also crashes the Unity editor, though it seems to happen less often than the compiled App, about 70% of the time.

The App runs totally fine being signed out (login screen at start) and also does not crash when there is no internet connection available (cable pulled, wifi deactivated) at the usual crash point after 1 hour runtime.

The Mac version is completely stable.

What I did try

As the crash timing is so exact, my first thought was that it is related to the ID token expiring. Thus I added code to manually refresh the ID token every 15 minutes. This did not fix the issue. As the only other thing we do on Firebase right now is getting some data from the Realtime Database after login we also tried the workaround from this issue -> https://github.com/firebase/quickstart-unity/issues/1284 disabling Persistence. This also did not fix our crashes.

Steps to reproduce:

Have you been able to reproduce this issue with just the Firebase Unity quickstarts (this GitHub project)? No, quickstarts has compiler errors in Unity 2021.3

What's the issue repro rate? (eg 100%, 1/5 etc) 90% compiled, 70% editor

What happened? How can we make the problem occur? Download our software from https://get.infinite-realms.de/2022/Setup_IR-2022.0.5.exe - install - sign up & in - wait 1 hour

Relevant Code:

Stack Trace v8.8.0:

0x00007FFB96235440 (FirebaseCppApp-8_8_0) Firebase_App_CSharp_InitializePlayServicesInternal
0x00007FFB96252E76 (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_FirebaseAnalytics
0x00007FFB96428256 (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB96436E46 (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB9642B13C (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB96430CDC (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB96399991 (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB9639ADFA (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB96439082 (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB96437A05 (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB96438B14 (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB96568801 (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB96569E0A (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB96312BAF (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB962BFB28 (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFB9673CC8D (FirebaseCppApp-8_8_0) SWIGRegisterStringCallback_StorageInternal
0x00007FFBEFE9244D (KERNEL32) BaseThreadInitThunk
0x00007FFBF094DFB8 (ntdll) RtlUserThreadStart

Weirdly FirebaseAnalytics shows up in the trace, although the module is not installed. We did try to install FirebaseAnalytics SDK afterwards because we thought maybe a call to the absent module caused the crash. This also did not fix the issue.

Stack Trace v10.1.0

PDB: 'C:\WINDOWS\system32\ncryptsslp.dll', fileVersion: 10.0.22621.1
  ERROR: SymGetSymFromAddr64, GetLastError: 'Es wurde versucht, auf eine unzulässige Adresse zuzugreifen.' (Address: 00007FFE15B38AEB)
0x00007FFE15B38AEB (FirebaseCppApp-10_1_0) (function-name not available)
0x00007FFE15C231F8 (FirebaseCppApp-10_1_0) uS::Node::getLoop
0x00007FFE15C3014D (FirebaseCppApp-10_1_0) uS::Node::getLoop
0x00007FFE15C24707 (FirebaseCppApp-10_1_0) uS::Node::getLoop
0x00007FFE15C289F5 (FirebaseCppApp-10_1_0) uS::Node::getLoop
0x00007FFE15C07828 (FirebaseCppApp-10_1_0) uS::Node::getLoop
0x00007FFE15C08B7A (FirebaseCppApp-10_1_0) uS::Node::getLoop
0x00007FFE15C323A0 (FirebaseCppApp-10_1_0) uS::Node::getLoop
0x00007FFE15C30D6A (FirebaseCppApp-10_1_0) uS::Node::getLoop
0x00007FFE15C31EBA (FirebaseCppApp-10_1_0) uS::Node::getLoop
0x00007FFE15C451BC (FirebaseCppApp-10_1_0) uS::Socket::write
0x00007FFE15C46EF3 (FirebaseCppApp-10_1_0) uWS::WebSocketState<0>::operator=
0x00007FFE15BA2964 (FirebaseCppApp-10_1_0) uS::Socket::freeMessage
0x00007FFE15BABEFF (FirebaseCppApp-10_1_0) uS::Socket::freeMessage
0x00007FFE85549363 (ucrtbase) recalloc
0x00007FFE874F244D (KERNEL32) BaseThreadInitThunk
0x00007FFE8814DFB8 (ntdll) RtlUserThreadStart

Login routines:

private IEnumerator Login(string email, string password)
        {
            var loginTask = Auth.SignInWithEmailAndPasswordAsync(email, password);

            yield return new WaitUntil(predicate: () => loginTask.IsCompleted);

            if (loginTask.Exception != null)
            {
                Debug.LogWarning(message: $"Failed to register task with {loginTask.Exception}");
            }
            else
            {
                User = loginTask.Result;

                if (User.IsEmailVerified)
                {
                    OnUserSignedIn?.Invoke();
                }
                else
                {
                    OpenVerificationMailPopup();
                }
            }
        }

private void TryResumeSession()
        {
            if (Auth.CurrentUser != null)
            {
                User = Auth.CurrentUser;

                if (User.IsEmailVerified)
                {
                    OnUserSignedIn?.Invoke();
                }
                else
                {
                    Logout();
                }
            }
        }

Realtime Database call example. We only do these after login. When offline and signed in via token the App loads settings from cache file instead of database:

        private IEnumerator LoadSettings()
        {
            // Abwarten bis der User eingelogged ist, vorher hat er keine Rechte auf der Datenbank
            yield return new WaitUntil(predicate: () => IsSignedIn());

            // Daten holen
            Task<DataSnapshot> databaseTask;
            var waitCount = 0;

            var versionName = Application.version.Replace('.', '-');
            databaseTask = DatabaseReference.Child("settings").Child(versionName).GetValueAsync();

            while (!databaseTask.IsCompleted)
            {
                if (databaseTask.Status == TaskStatus.WaitingForActivation)
                    waitCount++;

                // Fehler oder Timeout
                if (databaseTask.Exception != null || waitCount > 200)
                {
                    VersionSettings = ES3.Load<Dictionary<string, string>>("versionSettings", "firebase.pref");
                    SetOfflineMode();
                    break;
                }
                yield return null;
            }

            if (!OfflineMode)
            {
                // Datenbank verbunden, aktuelle Daten holen und abspeichern
                DataSnapshot snapshot = databaseTask.Result;
                VersionSettings = new Dictionary<string, string>();

                // Relevante Werte auslesen
                if (!VersionSettings.TryAdd("catalogRealms", snapshot.Child("catalog-realms").Value.ToString()))
                {
                    SetFatalError();
                    yield break;
                }
                if (!VersionSettings.TryAdd("catalogCore", snapshot.Child("catalog-core").Value.ToString()))
                {
                    SetFatalError();
                    yield break;
                }
                if (!VersionSettings.TryAdd("deprecated", snapshot.Child("deprecated").Value.ToString()))
                {
                    SetFatalError();
                    yield break;
                }

                // Version Settings für Offline-Modus cachen
                ES3.Save("versionSettings", VersionSettings, "firebase.pref");
            }

            OnSettingsLoaded?.Invoke();
        }

Attempt to fix the crash by manually refreshing ID token every 15 minutes:

        #region Token
        private void StartTokenRefreshTimer()
        {
            InvokeRepeating(nameof(TokenRefresh), 900, 900);
            OnUserSignedIn -= StartTokenRefreshTimer;
        }

        private void TokenRefresh()
        {
            Auth.CurrentUser.TokenAsync(true).ContinueWith(task => {
                if (task.IsCanceled) {
                    Debug.LogError("TokenAsync was canceled.");
                }
                else if (task.IsFaulted) {
                    Debug.LogError("TokenAsync encountered an error: " + task.Exception);
                }
            });
        }
        #endregion
paulinon commented 2 years ago

HI @megavoid,

Thanks for reporting this issue. Unfortunately, we couldn't test using the .exe file you provided due to security reasons. It would be helpful if you could share the Unity project so that we can identify what's causing this behavior.

Additionally, could you share the error message when using the quickstart? This may be caused by a missing dependency in your implementation.

megavoid commented 2 years ago

Hi @paulinon,

Thanks for responding. The Unity project is not public and cannot be shared. In the meantime I was able to fix the dependency issues with quickstart and am currently trying to reproduce the issue in that barebones project.

megavoid commented 2 years ago

After 2 more days of research there is some good news and some bad news. Bad news first: I have not been able to replicate the issue using the auth and database quickstart templates separately. My guess is that the issue is caused by the interplay of both the auth and database modules. I tried using the original database access code from our App in the database quickstart project without auth and it did not crash.

Good news is that I have found a workaround: After login our App makes three calls to the database module using GetValueAsync() on different RootReference children to load some settings. Once these have been completed I now call DatabaseReference.GoOffline();. This simple call prevents the App from crashing after the one hour delay.

I have also confirmed that the crash is being caused only with the database module being present. In one test I have simply ripped out all traces of the database module and replaced the required data with local constants. Our Firebase auth code was left untouched in this test. That worked perfectly fine without crashes.

I hope this in enough information to get a grip on the issue.

paulinon commented 2 years ago

Hi @megavoid,

Thanks for the update. I've consulted this with the team, and it turns out that the behavior you're facing is actually a bug. The crash that occurs after an hour may have something to do with Firebase ID tokens lasting that long.

That said, we'll be working on this. For now, could you confirm if you've set up a custom auth listener in your implementation?

megavoid commented 2 years ago

Hi @paulinon,

This is our Auth program flow, we are using a standard Unity Monobehaviour class:

private void Start()
        {
            Auth = null;
            User = null;

            TaskScheduler taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

            FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task =>
            {
                dependencyStatus = task.Result;
                if (dependencyStatus == DependencyStatus.Available)
                {
                    InitializeFirebase();
                }
                else
                {
                    Debug.LogError("Firebase Error, could not resolve Dependencies: " + dependencyStatus);
                }
            }, taskScheduler);
        }
private void InitializeFirebase()
        {
            Auth = FirebaseAuth.DefaultInstance;

            DatabaseReference = FirebaseDatabase.DefaultInstance.RootReference;

            FirebaseDatabase.DefaultInstance.SetPersistenceEnabled(false);

            OnFirebaseReady?.Invoke();
        }

The OnFirebaseReady event calls this function:

private void FirebaseReady()
        {
            TryResumeSession();
            OnFirebaseReady -= FirebaseReady;
        }
private void TryResumeSession()
        {
            if (Auth.CurrentUser != null)
            {
                User = Auth.CurrentUser;

                if (User.IsEmailVerified)
                {
                    OnUserSignedIn?.Invoke();
                }
                else
                {
                    // Reset values and display login screen
                    Logout();
                }
            }
        }