/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ShutdownPhase.h"
#ifdef XP_WIN
#  include <windows.h>

#  include "mozilla/PreXULSkeletonUI.h"
#else
#  include <unistd.h>
#endif

#include "AppShutdown.h"
#include "ProfilerControl.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/CmdLineAndEnvUtils.h"
#include "mozilla/LateWriteChecks.h"
#include "mozilla/PoisonIOInterposer.h"
#include "mozilla/Printf.h"
#include "mozilla/Services.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/StartupTimeline.h"
#include "mozilla/StaticPrefs_toolkit.h"
#include "mozilla/scache/StartupCache.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsAppRunner.h"
#include "nsDirectoryServiceUtils.h"
#include "nsExceptionHandler.h"
#include "nsICertStorage.h"
#include "nsThreadUtils.h"

// TODO: understand why on Android we cannot include this and if we should
#ifndef ANDROID
#  include "nsTerminator.h"
#endif
#include "prenv.h"

#ifdef MOZ_BACKGROUNDTASKS
#  include "mozilla/BackgroundTasks.h"
#endif

namespace mozilla {

const char* sPhaseObserverKeys[] = {
    nullptr,                            // NotInShutdown
    "quit-application",                 // AppShutdownConfirmed
    "profile-change-net-teardown",      // AppShutdownNetTeardown
    "profile-change-teardown",          // AppShutdownTeardown
    "profile-before-change",            // AppShutdown
    "profile-before-change-qm",         // AppShutdownQM
    "profile-before-change-telemetry",  // AppShutdownTelemetry
    "xpcom-will-shutdown",              // XPCOMWillShutdown
    "xpcom-shutdown",                   // XPCOMShutdown
    "xpcom-shutdown-threads",           // XPCOMShutdownThreads
    nullptr,                            // XPCOMShutdownFinal
    nullptr                             // CCPostLastCycleCollection
};

static_assert(sizeof(sPhaseObserverKeys) / sizeof(sPhaseObserverKeys[0]) ==
              (size_t)ShutdownPhase::ShutdownPhase_Length);

const char* sPhaseReadableNames[] = {"NotInShutdown",
                                     "AppShutdownConfirmed",
                                     "AppShutdownNetTeardown",
                                     "AppShutdownTeardown",
                                     "AppShutdown",
                                     "AppShutdownQM",
                                     "AppShutdownTelemetry",
                                     "XPCOMWillShutdown",
                                     "XPCOMShutdown",
                                     "XPCOMShutdownThreads",
                                     "XPCOMShutdownFinal",
                                     "CCPostLastCycleCollection"};

static_assert(sizeof(sPhaseReadableNames) / sizeof(sPhaseReadableNames[0]) ==
              (size_t)ShutdownPhase::ShutdownPhase_Length);

#ifndef ANDROID
static nsTerminator* sTerminator = nullptr;
#endif

static ShutdownPhase sFastShutdownPhase = ShutdownPhase::NotInShutdown;
static ShutdownPhase sLateWriteChecksPhase = ShutdownPhase::NotInShutdown;
static AppShutdownMode sShutdownMode = AppShutdownMode::Normal;
static Atomic<ShutdownPhase> sCurrentShutdownPhase(
    ShutdownPhase::NotInShutdown);
static int sExitCode = 0;
static Atomic<bool> sShutdownImpending(false);

// These environment variable strings are all deliberately copied and leaked
// due to requirements of PR_SetEnv and similar.
static char* sSavedXulAppFile = nullptr;
#ifdef XP_WIN
static wchar_t* sSavedProfDEnvVar = nullptr;
static wchar_t* sSavedProfLDEnvVar = nullptr;
#else
static char* sSavedProfDEnvVar = nullptr;
static char* sSavedProfLDEnvVar = nullptr;
#endif

bool AppShutdown::IsShutdownImpending() { return sShutdownImpending; }

void AppShutdown::SetImpendingShutdown() { sShutdownImpending = true; }

ShutdownPhase GetShutdownPhaseFromPrefValue(int32_t aPrefValue) {
  switch (aPrefValue) {
    case 1:
      return ShutdownPhase::CCPostLastCycleCollection;
    case 2:
      return ShutdownPhase::XPCOMShutdownThreads;
    case 3:
      return ShutdownPhase::XPCOMShutdown;
      // NOTE: the remaining values from the ShutdownPhase enum will be added
      // when we're at least reasonably confident that the world won't come
      // crashing down if we do a fast shutdown at that point.
  }
  return ShutdownPhase::NotInShutdown;
}

ShutdownPhase AppShutdown::GetCurrentShutdownPhase() {
  return sCurrentShutdownPhase;
}

bool AppShutdown::IsInOrBeyond(ShutdownPhase aPhase) {
  return (sCurrentShutdownPhase >= aPhase);
}

int AppShutdown::GetExitCode() { return sExitCode; }

void AppShutdown::SaveEnvVarsForPotentialRestart() {
  const char* s = PR_GetEnv("XUL_APP_FILE");
  if (s) {
    sSavedXulAppFile = Smprintf("%s=%s", "XUL_APP_FILE", s).release();
    MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(sSavedXulAppFile);
  }
}

const char* AppShutdown::GetObserverKey(ShutdownPhase aPhase) {
  return sPhaseObserverKeys[static_cast<std::underlying_type_t<ShutdownPhase>>(
      aPhase)];
}

const char* AppShutdown::GetShutdownPhaseName(ShutdownPhase aPhase) {
  return sPhaseReadableNames[static_cast<std::underlying_type_t<ShutdownPhase>>(
      aPhase)];
}

void AppShutdown::MaybeDoRestart() {
  if (sShutdownMode == AppShutdownMode::Restart) {
    StopLateWriteChecks();

    // Since we'll be launching our child while we're still alive, make sure
    // we've unlocked the profile first, otherwise the child could hit its
    // profile lock check before we've exited and thus released our lock.
    UnlockProfile();

    if (sSavedXulAppFile) {
      PR_SetEnv(sSavedXulAppFile);
    }

#ifdef XP_WIN
    if (sSavedProfDEnvVar && !EnvHasValue("XRE_PROFILE_PATH")) {
      SetEnvironmentVariableW(L"XRE_PROFILE_PATH", sSavedProfDEnvVar);
    }
    if (sSavedProfLDEnvVar && !EnvHasValue("XRE_PROFILE_LOCAL_PATH")) {
      SetEnvironmentVariableW(L"XRE_PROFILE_LOCAL_PATH", sSavedProfLDEnvVar);
    }
    (void)NotePreXULSkeletonUIRestarting();
#else
    if (sSavedProfDEnvVar && !EnvHasValue("XRE_PROFILE_PATH")) {
      PR_SetEnv(sSavedProfDEnvVar);
    }
    if (sSavedProfLDEnvVar && !EnvHasValue("XRE_PROFILE_LOCAL_PATH")) {
      PR_SetEnv(sSavedProfLDEnvVar);
    }
#endif

    LaunchChild(true);
  }
}

#ifdef XP_WIN
wchar_t* CopyPathIntoNewWCString(nsIFile* aFile) {
  wchar_t* result = nullptr;
  nsAutoString resStr;
  aFile->GetPath(resStr);
  if (resStr.Length() > 0) {
    result = (wchar_t*)malloc((resStr.Length() + 1) * sizeof(wchar_t));
    if (result) {
      wcscpy(result, resStr.get());
      result[resStr.Length()] = 0;
    }
  }

  return result;
}
#endif

void AppShutdown::Init(AppShutdownMode aMode, int aExitCode,
                       AppShutdownReason aReason) {
  if (sShutdownMode == AppShutdownMode::Normal) {
    sShutdownMode = aMode;
  }
  AppShutdown::AnnotateShutdownReason(aReason);
  AppShutdown::SetImpendingShutdown();

  sExitCode = aExitCode;

#ifndef ANDROID
  sTerminator = new nsTerminator();
#endif

  // Late-write checks needs to find the profile directory, so it has to
  // be initialized before services::Shutdown or (because of
  // xpcshell tests replacing the service) modules being unloaded.
  InitLateWriteChecks();

  int32_t fastShutdownPref = StaticPrefs::toolkit_shutdown_fastShutdownStage();
  sFastShutdownPhase = GetShutdownPhaseFromPrefValue(fastShutdownPref);
  int32_t lateWriteChecksPref =
      StaticPrefs::toolkit_shutdown_lateWriteChecksStage();
  sLateWriteChecksPhase = GetShutdownPhaseFromPrefValue(lateWriteChecksPref);

  // Very early shutdowns can happen before the startup cache is even
  // initialized; don't bother initializing it during shutdown.
  if (auto* cache = scache::StartupCache::GetSingletonNoInit()) {
    cache->MaybeKickOffShutdownWrite();
  }
}

void AppShutdown::MaybeFastShutdown(ShutdownPhase aPhase) {
  // For writes which we want to ensure are recorded, we don't want to trip
  // the late write checking code. Anything that writes to disk and which
  // we don't want to skip should be listed out explicitly in this section.
  if (aPhase == sFastShutdownPhase || aPhase == sLateWriteChecksPhase) {
    if (auto* cache = scache::StartupCache::GetSingletonNoInit()) {
      cache->EnsureShutdownWriteComplete();
    }

    nsresult rv;
    nsCOMPtr<nsICertStorage> certStorage =
        do_GetService("@mozilla.org/security/certstorage;1", &rv);
    if (NS_SUCCEEDED(rv)) {
      SpinEventLoopUntil("AppShutdown::MaybeFastShutdown"_ns, [&]() {
        int32_t remainingOps;
        nsresult rv = certStorage->GetRemainingOperationCount(&remainingOps);
        NS_ASSERTION(NS_SUCCEEDED(rv),
                     "nsICertStorage::getRemainingOperationCount failed during "
                     "shutdown");
        return NS_FAILED(rv) || remainingOps <= 0;
      });
    }
  }
  if (aPhase == sFastShutdownPhase) {
    StopLateWriteChecks();
    RecordShutdownEndTimeStamp();
    MaybeDoRestart();

    profiler_shutdown(IsFastShutdown::Yes);

    DoImmediateExit(sExitCode);
  } else if (aPhase == sLateWriteChecksPhase) {
#ifdef XP_MACOSX
    OnlyReportDirtyWrites();
#endif /* XP_MACOSX */
    BeginLateWriteChecks();
  }
}

void AppShutdown::OnShutdownConfirmed() {
  // If we're restarting, we need to save environment variables correctly
  // while everything is still alive to do so.
  if (sShutdownMode == AppShutdownMode::Restart) {
    nsCOMPtr<nsIFile> profD;
    nsCOMPtr<nsIFile> profLD;
    NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(profD));
    NS_GetSpecialDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR,
                           getter_AddRefs(profLD));
#ifdef XP_WIN
    sSavedProfDEnvVar = CopyPathIntoNewWCString(profD);
    sSavedProfLDEnvVar = CopyPathIntoNewWCString(profLD);
#else
    nsAutoCString profDStr;
    profD->GetNativePath(profDStr);
    sSavedProfDEnvVar =
        Smprintf("XRE_PROFILE_PATH=%s", profDStr.get()).release();
    nsAutoCString profLDStr;
    profLD->GetNativePath(profLDStr);
    sSavedProfLDEnvVar =
        Smprintf("XRE_PROFILE_LOCAL_PATH=%s", profLDStr.get()).release();
#endif
    MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(sSavedProfDEnvVar);
    MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(sSavedProfLDEnvVar);
  }
}

void AppShutdown::DoImmediateExit(int aExitCode) {
#ifdef XP_WIN
  HANDLE process = ::GetCurrentProcess();
  if (::TerminateProcess(process, aExitCode)) {
    ::WaitForSingleObject(process, INFINITE);
  }
  MOZ_CRASH("TerminateProcess failed.");
#else
  _exit(aExitCode);
#endif
}

bool AppShutdown::IsRestarting() {
  return sShutdownMode == AppShutdownMode::Restart;
}

void AppShutdown::AnnotateShutdownReason(AppShutdownReason aReason) {
  auto key = CrashReporter::Annotation::ShutdownReason;
  const char* reasonStr;
  switch (aReason) {
    case AppShutdownReason::AppClose:
      reasonStr = "AppClose";
      break;
    case AppShutdownReason::AppRestart:
      reasonStr = "AppRestart";
      break;
    case AppShutdownReason::OSForceClose:
      reasonStr = "OSForceClose";
      break;
    case AppShutdownReason::OSSessionEnd:
      reasonStr = "OSSessionEnd";
      break;
    case AppShutdownReason::OSShutdown:
      reasonStr = "OSShutdown";
      break;
    default:
      MOZ_ASSERT_UNREACHABLE("We should know the given reason for shutdown.");
      reasonStr = "Unknown";
      break;
  }
  CrashReporter::RecordAnnotationCString(key, reasonStr);
}

#ifdef DEBUG
static bool sNotifyingShutdownObservers = false;
static bool sAdvancingShutdownPhase = false;

bool AppShutdown::IsNoOrLegalShutdownTopic(const char* aTopic) {
  if (!XRE_IsParentProcess()) {
    // Until we know what to do with AppShutdown for child processes,
    // we ignore them for now. See bug 1697745.
    return true;
  }
  ShutdownPhase phase = GetShutdownPhaseFromTopic(aTopic);
  return phase == ShutdownPhase::NotInShutdown ||
         (sNotifyingShutdownObservers && phase == sCurrentShutdownPhase);
}
#endif

void AppShutdown::AdvanceShutdownPhaseInternal(
    ShutdownPhase aPhase, bool doNotify, const char16_t* aNotificationData,
    const nsCOMPtr<nsISupports>& aNotificationSubject) {
  AssertIsOnMainThread();
#ifdef DEBUG
  // Prevent us from re-entrance
  MOZ_ASSERT(!sAdvancingShutdownPhase);
  sAdvancingShutdownPhase = true;
  auto exit = MakeScopeExit([] { sAdvancingShutdownPhase = false; });
#endif

  // We ensure that we can move only forward. We cannot
  // MOZ_ASSERT here as there are some tests that fire
  // notifications out of shutdown order.
  // See for example test_sss_sanitizeOnShutdown.js
  if (sCurrentShutdownPhase >= aPhase) {
    return;
  }

  // In case we missed the earlier Init (in the parent) or notification (in
  // content processes), we ensure the flag is set from now.
  // This should only ever be needed in some test environments, but it's cheap
  // enough to just do it always.
  SetImpendingShutdown();

  nsCOMPtr<nsIThread> thread = do_GetCurrentThread();

  // AppShutdownConfirmed is special in some ways as
  // - we can be called on top of a nested event loop (and it is the phase for
  //   which SpinEventLoopUntilOrQuit breaks, so we want to return soon)
  // - we can be called from a sync marionette function that wants immediate
  //   feedback, too
  // - in general, AppShutdownConfirmed will fire the "quit-application"
  //   notification which in turn will cause an event to be dispatched that
  //   runs all the rest of our shutdown sequence which we do not want to be
  //   processed on top of the running event.
  // Thus we never do any NS_ProcessPendingEvents for it.
  bool mayProcessPending = (aPhase > ShutdownPhase::AppShutdownConfirmed);

  // Give runnables dispatched between two calls to AdvanceShutdownPhase
  // a chance to run before actually advancing the phase. As we excluded
  // AppShutdownConfirmed above we can be sure that the processing is
  // covered by the terminator's timer of the previous phase during normal
  // shutdown (except out-of-order calls from some test).
  // Note that this affects only main thread runnables, such that the correct
  // way of ensuring shutdown processing remains to have an async shutdown
  // blocker.
  if (mayProcessPending && thread) {
    NS_ProcessPendingEvents(thread);
  }

  // From now on any IsInOrBeyond checks will find the new phase set.
  sCurrentShutdownPhase = aPhase;

#ifndef ANDROID
  if (sTerminator) {
    sTerminator->AdvancePhase(aPhase);
  }
#endif

  AppShutdown::MaybeFastShutdown(aPhase);

  // This will null out the gathered pointers for this phase synchronously.
  // Note that we keep the old order here to avoid breakage, so be aware that
  // the notifications fired below will find these already cleared in case
  // you expected the opposite.
  mozilla::KillClearOnShutdown(aPhase);

  // Empty our MT event queue to process any side effects thereof.
  if (mayProcessPending && thread) {
    NS_ProcessPendingEvents(thread);
  }

  if (doNotify) {
    const char* aTopic = AppShutdown::GetObserverKey(aPhase);
    if (aTopic) {
      nsCOMPtr<nsIObserverService> obsService =
          mozilla::services::GetObserverService();
      if (obsService) {
#ifdef DEBUG
        sNotifyingShutdownObservers = true;
        auto reset = MakeScopeExit([] { sNotifyingShutdownObservers = false; });
#endif
        obsService->NotifyObservers(aNotificationSubject, aTopic,
                                    aNotificationData);
        // Empty our MT event queue again after the notification has finished
        if (mayProcessPending && thread) {
          NS_ProcessPendingEvents(thread);
        }
      }
    }
  }
}

/**
 * XXX: Before tackling bug 1697745 we need the
 * possibility to advance the phase without notification
 * in the content process.
 */
void AppShutdown::AdvanceShutdownPhaseWithoutNotify(ShutdownPhase aPhase) {
  AdvanceShutdownPhaseInternal(aPhase, /* doNotify */ false, nullptr, nullptr);
}

void AppShutdown::AdvanceShutdownPhase(
    ShutdownPhase aPhase, const char16_t* aNotificationData,
    const nsCOMPtr<nsISupports>& aNotificationSubject) {
  AdvanceShutdownPhaseInternal(aPhase, /* doNotify */ true, aNotificationData,
                               aNotificationSubject);
}

ShutdownPhase AppShutdown::GetShutdownPhaseFromTopic(const char* aTopic) {
  for (size_t i = 0; i < std::size(sPhaseObserverKeys); ++i) {
    if (sPhaseObserverKeys[i] && !strcmp(sPhaseObserverKeys[i], aTopic)) {
      return static_cast<ShutdownPhase>(i);
    }
  }
  return ShutdownPhase::NotInShutdown;
}

}  // namespace mozilla
