diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/xre/nsUpdateDriver.cpp | 1081 |
1 files changed, 1081 insertions, 0 deletions
diff --git a/toolkit/xre/nsUpdateDriver.cpp b/toolkit/xre/nsUpdateDriver.cpp new file mode 100644 index 0000000000..bd2d4d3e59 --- /dev/null +++ b/toolkit/xre/nsUpdateDriver.cpp @@ -0,0 +1,1081 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 <stdlib.h> +#include <stdio.h> +#include "nsUpdateDriver.h" +#include "nsXULAppAPI.h" +#include "nsAppRunner.h" +#include "nsIFile.h" +#include "nsVariant.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "prproces.h" +#include "mozilla/Logging.h" +#include "prenv.h" +#include "nsVersionComparator.h" +#include "nsDirectoryServiceDefs.h" +#include "nsThreadUtils.h" +#include "nsIXULAppInfo.h" +#include "mozilla/Preferences.h" +#include "nsPrintfCString.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Printf.h" +#include "mozilla/UniquePtr.h" +#include "nsIObserverService.h" +#include "nsNetCID.h" +#include "mozilla/Services.h" + +#ifdef XP_MACOSX +# include "nsILocalFileMac.h" +# include "nsCommandLineServiceMac.h" +# include "MacLaunchHelper.h" +# include "updaterfileutils_osx.h" +# include "mozilla/Monitor.h" +#endif + +#if defined(XP_WIN) +# include <direct.h> +# include <process.h> +# include <windows.h> +# include <shlwapi.h> +# include <strsafe.h> +# include "commonupdatedir.h" +# include "nsWindowsHelpers.h" +# define getcwd(path, size) _getcwd(path, size) +# define getpid() GetCurrentProcessId() +#elif defined(XP_UNIX) +# include <unistd.h> +# include <sys/wait.h> +#endif + +using namespace mozilla; + +static LazyLogModule sUpdateLog("updatedriver"); +// Some other file in our unified batch might have defined LOG already. +#ifdef LOG +# undef LOG +#endif +#define LOG(args) MOZ_LOG(sUpdateLog, mozilla::LogLevel::Debug, args) + +#ifdef XP_WIN +# define UPDATER_BIN "updater.exe" +# define MAINTENANCE_SVC_NAME L"MozillaMaintenance" +#elif XP_MACOSX +# define UPDATER_APP "updater.app" +# define UPDATER_BIN "org.mozilla.updater" +#else +# define UPDATER_BIN "updater" +#endif + +#ifdef XP_MACOSX +static void UpdateDriverSetupMacCommandLine(int& argc, char**& argv, + bool restart) { + if (NS_IsMainThread()) { + CommandLineServiceMac::SetupMacCommandLine(argc, argv, restart); + return; + } + // Bug 1335916: SetupMacCommandLine calls a CoreFoundation function that + // asserts that it was called from the main thread, so if we are not the main + // thread, we have to dispatch that call to there. But we also have to get the + // result from it, so we can't just dispatch and return, we have to wait + // until the dispatched operation actually completes. So we also set up a + // monitor to signal us when that happens, and block until then. + Monitor monitor("nsUpdateDriver SetupMacCommandLine"); + + nsresult rv = NS_DispatchToMainThread(NS_NewRunnableFunction( + "UpdateDriverSetupMacCommandLine", + [&argc, &argv, restart, &monitor]() -> void { + CommandLineServiceMac::SetupMacCommandLine(argc, argv, restart); + MonitorAutoLock(monitor).Notify(); + })); + + if (NS_FAILED(rv)) { + LOG( + ("Update driver error dispatching SetupMacCommandLine to main thread: " + "%d\n", + rv)); + return; + } + + // The length of this wait is arbitrary, but should be long enough that having + // it expire means something is seriously wrong. + CVStatus status = + MonitorAutoLock(monitor).Wait(TimeDuration::FromSeconds(60)); + if (status == CVStatus::Timeout) { + LOG(("Update driver timed out waiting for SetupMacCommandLine\n")); + } +} +#endif + +static nsresult GetCurrentWorkingDir(nsACString& aOutPath) { + // Cannot use NS_GetSpecialDirectory because XPCOM is not yet initialized. + // This code is duplicated from xpcom/io/SpecialSystemDirectory.cpp: + + aOutPath.Truncate(); + +#if defined(XP_WIN) + wchar_t wpath[MAX_PATH]; + if (!_wgetcwd(wpath, ArrayLength(wpath))) { + return NS_ERROR_FAILURE; + } + CopyUTF16toUTF8(nsDependentString(wpath), aOutPath); +#else + char path[MAXPATHLEN]; + if (!getcwd(path, ArrayLength(path))) { + return NS_ERROR_FAILURE; + } + aOutPath = path; +#endif + + return NS_OK; +} + +/** + * Get the path to the installation directory. For Mac OS X this will be the + * bundle directory. + * + * @param appDir the application directory file object + * @param installDirPath the path to the installation directory + */ +static nsresult GetInstallDirPath(nsIFile* appDir, nsACString& installDirPath) { + nsresult rv; +#ifdef XP_MACOSX + nsCOMPtr<nsIFile> parentDir1, parentDir2; + rv = appDir->GetParent(getter_AddRefs(parentDir1)); + NS_ENSURE_SUCCESS(rv, rv); + rv = parentDir1->GetParent(getter_AddRefs(parentDir2)); + NS_ENSURE_SUCCESS(rv, rv); + rv = parentDir2->GetNativePath(installDirPath); + NS_ENSURE_SUCCESS(rv, rv); +#elif XP_WIN + nsAutoString installDirPathW; + rv = appDir->GetPath(installDirPathW); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF16toUTF8(installDirPathW, installDirPath); +#else + rv = appDir->GetNativePath(installDirPath); + NS_ENSURE_SUCCESS(rv, rv); +#endif + return NS_OK; +} + +static bool GetFile(nsIFile* dir, const nsACString& name, + nsCOMPtr<nsIFile>& result) { + nsresult rv; + + nsCOMPtr<nsIFile> file; + rv = dir->Clone(getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return false; + } + + rv = file->AppendNative(name); + if (NS_FAILED(rv)) { + return false; + } + + result = file; + return true; +} + +static bool GetStatusFile(nsIFile* dir, nsCOMPtr<nsIFile>& result) { + return GetFile(dir, "update.status"_ns, result); +} + +/** + * Get the contents of the update.status file when the update.status file can + * be opened with read and write access. The reason it is opened for both read + * and write is to prevent trying to update when the user doesn't have write + * access to the update directory. + * + * @param statusFile the status file object. + * @param buf the buffer holding the file contents + * + * @return true if successful, false otherwise. + */ +template <size_t Size> +static bool GetStatusFileContents(nsIFile* statusFile, char (&buf)[Size]) { + static_assert( + Size > 16, + "Buffer needs to be large enough to hold the known status codes"); + + PRFileDesc* fd = nullptr; + nsresult rv = statusFile->OpenNSPRFileDesc(PR_RDWR, 0660, &fd); + if (NS_FAILED(rv)) { + return false; + } + + const int32_t n = PR_Read(fd, buf, Size); + PR_Close(fd); + + return (n >= 0); +} + +typedef enum { + eNoUpdateAction, + ePendingUpdate, + ePendingService, + ePendingElevate, + eAppliedUpdate, + eAppliedService, +} UpdateStatus; + +/** + * Returns a value indicating what needs to be done in order to handle an + * update. + * + * @param dir the directory in which we should look for an update.status file. + * @param statusFile the update.status file found in the directory. + * + * @return the update action to be performed. + */ +static UpdateStatus GetUpdateStatus(nsIFile* dir, + nsCOMPtr<nsIFile>& statusFile) { + if (GetStatusFile(dir, statusFile)) { + char buf[32]; + if (GetStatusFileContents(statusFile, buf)) { + const char kPending[] = "pending"; + const char kPendingService[] = "pending-service"; + const char kPendingElevate[] = "pending-elevate"; + const char kApplied[] = "applied"; + const char kAppliedService[] = "applied-service"; + if (!strncmp(buf, kPendingElevate, sizeof(kPendingElevate) - 1)) { + return ePendingElevate; + } + if (!strncmp(buf, kPendingService, sizeof(kPendingService) - 1)) { + return ePendingService; + } + if (!strncmp(buf, kPending, sizeof(kPending) - 1)) { + return ePendingUpdate; + } + if (!strncmp(buf, kAppliedService, sizeof(kAppliedService) - 1)) { + return eAppliedService; + } + if (!strncmp(buf, kApplied, sizeof(kApplied) - 1)) { + return eAppliedUpdate; + } + } + } + return eNoUpdateAction; +} + +static bool GetVersionFile(nsIFile* dir, nsCOMPtr<nsIFile>& result) { + return GetFile(dir, "update.version"_ns, result); +} + +// Compares the current application version with the update's application +// version. +static bool IsOlderVersion(nsIFile* versionFile, const char* appVersion) { + PRFileDesc* fd = nullptr; + nsresult rv = versionFile->OpenNSPRFileDesc(PR_RDONLY, 0660, &fd); + if (NS_FAILED(rv)) { + return true; + } + + char buf[32]; + const int32_t n = PR_Read(fd, buf, sizeof(buf)); + PR_Close(fd); + + if (n < 0) { + return false; + } + + // Trim off the trailing newline + if (buf[n - 1] == '\n') { + buf[n - 1] = '\0'; + } + + // If the update xml doesn't provide the application version the file will + // contain the string "null" and it is assumed that the update is not older. + const char kNull[] = "null"; + if (strncmp(buf, kNull, sizeof(kNull) - 1) == 0) { + return false; + } + + return mozilla::Version(appVersion) > buf; +} + +/** + * Applies, switches, or stages an update. + * + * @param greDir the GRE directory + * @param updateDir the update root directory + * @param appDir the application directory + * @param appArgc the number of args passed to the application + * @param appArgv the args passed to the application + * (used for restarting the application when necessary) + * @param restart true when a restart is necessary. + * @param isStaged true when the update has already been staged + * @param outpid (out) parameter holding the handle to the updater application + * when staging updates + */ +static void ApplyUpdate(nsIFile* greDir, nsIFile* updateDir, nsIFile* appDir, + int appArgc, char** appArgv, bool restart, + bool isStaged, ProcessType* outpid) { + // The following determines the update operation to perform. + // 1. When restart is false the update will be staged. + // 2. When restart is true and isStaged is false the update will apply the mar + // file to the installation directory. + // 3. When restart is true and isStaged is true the update will switch the + // staged update with the installation directory. + + nsresult rv; + + nsCOMPtr<nsIFile> updater; + nsAutoCString updaterPath; + nsAutoCString updateDirPath; +#if defined(XP_WIN) + // Get an nsIFile reference for the updater in the installation dir. + if (!GetFile(greDir, nsLiteralCString(UPDATER_BIN), updater)) { + return; + } + + // Get the path to the updater. + nsAutoString updaterPathW; + rv = updater->GetPath(updaterPathW); + if (NS_FAILED(rv)) { + return; + } + CopyUTF16toUTF8(updaterPathW, updaterPath); + + // Get the path to the update dir. + nsAutoString updateDirPathW; + rv = updateDir->GetPath(updateDirPathW); + if (NS_FAILED(rv)) { + return; + } + CopyUTF16toUTF8(updateDirPathW, updateDirPath); +#elif defined(XP_MACOSX) + // Get an nsIFile reference for the updater in the installation dir. + if (!GetFile(appDir, nsLiteralCString(UPDATER_APP), updater)) { + return; + } + rv = updater->AppendNative("Contents"_ns); + if (NS_FAILED(rv)) { + return; + } + rv = updater->AppendNative("MacOS"_ns); + if (NS_FAILED(rv)) { + return; + } + rv = updater->AppendNative(nsLiteralCString(UPDATER_BIN)); + if (NS_FAILED(rv)) { + return; + } + + // Get the path to the updater. + rv = updater->GetNativePath(updaterPath); + if (NS_FAILED(rv)) { + return; + } + + // Get the path to the update dir. + rv = updateDir->GetNativePath(updateDirPath); + if (NS_FAILED(rv)) { + return; + } +#else + // Get an nsIFile reference for the updater in the installation dir. + if (!GetFile(greDir, nsLiteralCString(UPDATER_BIN), updater)) { + return; + } + + // Get the path to the updater. + rv = updater->GetNativePath(updaterPath); + if (NS_FAILED(rv)) { + return; + } + + // Get the path to the update dir. + rv = updateDir->GetNativePath(updateDirPath); + if (NS_FAILED(rv)) { + return; + } +#endif + + // appFilePath and workingDirPath are only used when the application will be + // restarted. + nsAutoCString appFilePath; + nsAutoCString workingDirPath; + if (restart) { + // Get the path to the current working directory. + rv = GetCurrentWorkingDir(workingDirPath); + if (NS_FAILED(rv)) { + return; + } + + // Get the application file path used by the updater to restart the + // application after the update has finished. + nsCOMPtr<nsIFile> appFile; + XRE_GetBinaryPath(getter_AddRefs(appFile)); + if (!appFile) { + return; + } + +#if defined(XP_WIN) + nsAutoString appFilePathW; + rv = appFile->GetPath(appFilePathW); + if (NS_FAILED(rv)) { + return; + } + CopyUTF16toUTF8(appFilePathW, appFilePath); +#else + rv = appFile->GetNativePath(appFilePath); + if (NS_FAILED(rv)) { + return; + } +#endif + } + + // Get the installation directory path. + nsAutoCString installDirPath; + rv = GetInstallDirPath(appDir, installDirPath); + if (NS_FAILED(rv)) { + return; + } + + nsAutoCString applyToDirPath; + nsCOMPtr<nsIFile> updatedDir; + if (restart && !isStaged) { + // The install directory is the same as the apply to directory. + applyToDirPath.Assign(installDirPath); + } else { + // Get the directory where the update is staged or will be staged. +#if defined(XP_MACOSX) + if (!GetFile(updateDir, "Updated.app"_ns, updatedDir)) { +#else + if (!GetFile(appDir, "updated"_ns, updatedDir)) { +#endif + return; + } +#if defined(XP_WIN) + nsAutoString applyToDirPathW; + rv = updatedDir->GetPath(applyToDirPathW); + if (NS_FAILED(rv)) { + return; + } + CopyUTF16toUTF8(applyToDirPathW, applyToDirPath); +#else + rv = updatedDir->GetNativePath(applyToDirPath); +#endif + } + if (NS_FAILED(rv)) { + return; + } + + if (restart && isStaged) { + // When the update should already be staged make sure that the updated + // directory exists. + bool updatedDirExists = false; + if (NS_FAILED(updatedDir->Exists(&updatedDirExists)) || !updatedDirExists) { + return; + } + } + + // On platforms where we are not calling execv, we may need to make the + // updater executable wait for the calling process to exit. Otherwise, the + // updater may have trouble modifying our executable image (because it might + // still be in use). This is accomplished by passing our PID to the updater + // so that it can wait for us to exit. This is not perfect as there is a race + // condition that could bite us. It's possible that the calling process could + // exit before the updater waits on the specified PID, and in the meantime a + // new process with the same PID could be created. This situation is + // unlikely, however, given the way most operating systems recycle PIDs. We'll + // take our chances ;-) Construct the PID argument for this process to pass to + // the updater. + nsAutoCString pid; + if (restart) { +#if defined(XP_UNIX) & !defined(XP_MACOSX) + // When execv is used for an update that requires a restart 0 is passed + // which is ignored by the updater. + pid.AssignLiteral("0"); +#else + pid.AppendInt((int32_t)getpid()); +#endif + if (isStaged) { + // Append a special token to the PID in order to inform the updater that + // it should replace install with the updated directory. + pid.AppendLiteral("/replace"); + } + } else { + // Signal the updater application that it should stage the update. + pid.AssignLiteral("-1"); + } + + int argc = 5; + if (restart) { + argc = appArgc + 6; + if (gRestartedByOS) { + argc += 1; + } + } + char** argv = static_cast<char**>(malloc((argc + 1) * sizeof(char*))); + if (!argv) { + return; + } + argv[0] = (char*)updaterPath.get(); + argv[1] = (char*)updateDirPath.get(); + argv[2] = (char*)installDirPath.get(); + argv[3] = (char*)applyToDirPath.get(); + argv[4] = (char*)pid.get(); + if (restart && appArgc) { + argv[5] = (char*)workingDirPath.get(); + argv[6] = (char*)appFilePath.get(); + for (int i = 1; i < appArgc; ++i) { + argv[6 + i] = appArgv[i]; + } + if (gRestartedByOS) { + // We haven't truly started up, restore this argument so that we will have + // it upon restart. + argv[6 + appArgc] = const_cast<char*>("-os-restarted"); + } + } + argv[argc] = nullptr; + + if (restart && gSafeMode) { + PR_SetEnv("MOZ_SAFE_MODE_RESTART=1"); + } + + LOG(("spawning updater process [%s]\n", updaterPath.get())); + +#if defined(XP_UNIX) && !defined(XP_MACOSX) + // We use execv to spawn the updater process on all UNIX systems except Mac + // OSX since it is known to cause problems on the Mac. Windows has execv, but + // it is a faked implementation that doesn't really replace the current + // process. Instead it spawns a new process, so we gain nothing from using + // execv on Windows. + if (restart) { + int execResult = execv(updaterPath.get(), argv); + free(argv); + exit(execResult); + } + *outpid = fork(); + if (*outpid == -1) { + free(argv); + return; + } + if (*outpid == 0) { + int execResult = execv(updaterPath.get(), argv); + free(argv); + exit(execResult); + } +#elif defined(XP_WIN) + if (isStaged) { + // Launch the updater to replace the installation with the staged updated. + if (!WinLaunchChild(updaterPathW.get(), argc, argv)) { + free(argv); + return; + } + } else { + // Launch the updater to either stage or apply an update. + if (!WinLaunchChild(updaterPathW.get(), argc, argv, nullptr, outpid)) { + free(argv); + return; + } + } +#elif defined(XP_MACOSX) +UpdateDriverSetupMacCommandLine(argc, argv, restart); +// We need to detect whether elevation is required for this update. This can +// occur when an admin user installs the application, but another admin +// user attempts to update (see bug 394984). +if (restart && !IsRecursivelyWritable(installDirPath.get())) { + bool hasLaunched = LaunchElevatedUpdate(argc, argv, outpid); + free(argv); + if (!hasLaunched) { + LOG(("Failed to launch elevated update!")); + exit(1); + } + exit(0); +} + +if (isStaged) { + // Launch the updater to replace the installation with the staged updated. + LaunchChildMac(argc, argv); +} else { + // Launch the updater to either stage or apply an update. + LaunchChildMac(argc, argv, outpid); +} +#else +if (isStaged) { + // Launch the updater to replace the installation with the staged updated. + PR_CreateProcessDetached(updaterPath.get(), argv, nullptr, nullptr); +} else { + // Launch the updater to either stage or apply an update. + *outpid = PR_CreateProcess(updaterPath.get(), argv, nullptr, nullptr); +} +#endif + free(argv); + if (restart) { + exit(0); + } +} + +/** + * Wait briefly to see if a process terminates, then return true if it has. + */ +static bool ProcessHasTerminated(ProcessType pt) { +#if defined(XP_WIN) + if (WaitForSingleObject(pt, 1000)) { + return false; + } + CloseHandle(pt); + return true; +#elif defined(XP_MACOSX) + // We're waiting for the process to terminate in LaunchChildMac. + return true; +#elif defined(XP_UNIX) + int exitStatus; + pid_t exited = waitpid(pt, &exitStatus, WNOHANG); + if (exited == 0) { + // Process is still running. + sleep(1); + return false; + } + if (exited == -1) { + LOG(("Error while checking if the updater process is finished")); + // This shouldn't happen, but if it does, the updater process is lost to us, + // so the best we can do is pretend that it's exited. + return true; + } + // If we get here, the process has exited; make sure it exited normally. + if (WIFEXITED(exitStatus) && (WEXITSTATUS(exitStatus) != 0)) { + LOG(("Error while running the updater process, check update.log")); + } + return true; +#else + // No way to have a non-blocking implementation on these platforms, + // because we're using NSPR and it only provides a blocking wait. + int32_t exitCode; + PR_WaitProcess(pt, &exitCode); + if (exitCode != 0) { + LOG(("Error while running the updater process, check update.log")); + } + return true; +#endif +} + +nsresult ProcessUpdates(nsIFile* greDir, nsIFile* appDir, nsIFile* updRootDir, + int argc, char** argv, const char* appVersion, + bool restart, ProcessType* pid) { + nsresult rv; + + nsCOMPtr<nsIFile> updatesDir; + rv = updRootDir->Clone(getter_AddRefs(updatesDir)); + NS_ENSURE_SUCCESS(rv, rv); + rv = updatesDir->AppendNative("updates"_ns); + NS_ENSURE_SUCCESS(rv, rv); + rv = updatesDir->AppendNative("0"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + // Return early since there isn't a valid update when the update application + // version file doesn't exist or if the update's application version is less + // than the current application version. The cleanup of the update will happen + // during post update processing in nsUpdateService.js. + nsCOMPtr<nsIFile> versionFile; + if (!GetVersionFile(updatesDir, versionFile) || + IsOlderVersion(versionFile, appVersion)) { + return NS_OK; + } + + nsCOMPtr<nsIFile> statusFile; + UpdateStatus status = GetUpdateStatus(updatesDir, statusFile); + switch (status) { + case ePendingUpdate: + case ePendingService: { + ApplyUpdate(greDir, updatesDir, appDir, argc, argv, restart, false, pid); + break; + } + case eAppliedUpdate: + case eAppliedService: + // An update was staged and needs to be switched so the updated + // application is used. + ApplyUpdate(greDir, updatesDir, appDir, argc, argv, restart, true, pid); + break; + case ePendingElevate: + // No action should be performed since the user hasn't opted into + // elevating for the update so continue application startup. + case eNoUpdateAction: + // We don't need to do any special processing here, we'll just continue to + // startup the application. + break; + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsUpdateProcessor, nsIUpdateProcessor) + +nsUpdateProcessor::nsUpdateProcessor() : mUpdaterPID(0) {} + +nsUpdateProcessor::~nsUpdateProcessor() = default; + +NS_IMETHODIMP +nsUpdateProcessor::ProcessUpdate() { + nsresult rv; + + nsCOMPtr<nsIProperties> ds = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> exeFile; + rv = ds->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile), + getter_AddRefs(exeFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> appDir; + rv = exeFile->GetParent(getter_AddRefs(appDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> greDir; + rv = ds->Get(NS_GRE_DIR, NS_GET_IID(nsIFile), getter_AddRefs(greDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> updRoot; + rv = ds->Get(XRE_UPDATE_ROOT_DIR, NS_GET_IID(nsIFile), + getter_AddRefs(updRoot)); + NS_ASSERTION(NS_SUCCEEDED(rv), "Can't get the UpdRootD dir"); + + // XRE_UPDATE_ROOT_DIR should not fail but if it does fallback to the + // application directory just to be safe. + if (NS_FAILED(rv)) { + rv = appDir->Clone(getter_AddRefs(updRoot)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIXULAppInfo> appInfo = + do_GetService("@mozilla.org/xre/app-info;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString appVersion; + rv = appInfo->GetVersion(appVersion); + NS_ENSURE_SUCCESS(rv, rv); + + // Copy the parameters to the StagedUpdateInfo structure shared with the + // watcher thread. + mInfo.mGREDir = greDir; + mInfo.mAppDir = appDir; + mInfo.mUpdateRoot = updRoot; + mInfo.mArgc = 0; + mInfo.mArgv = nullptr; + mInfo.mAppVersion = appVersion; + + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod("nsUpdateProcessor::StartStagedUpdate", this, + &nsUpdateProcessor::StartStagedUpdate); + return NS_NewNamedThread("Update Watcher", getter_AddRefs(mProcessWatcher), + r); +} + +NS_IMETHODIMP +nsUpdateProcessor::FixUpdateDirectoryPerms(bool aUseServiceOnFailure) { +#ifndef XP_WIN + return NS_ERROR_NOT_IMPLEMENTED; +#else + enum class State { + Initializing, + WaitingToStart, + Starting, + WaitingForFinish, + }; + + class FixUpdateDirectoryPermsRunnable final : public mozilla::Runnable { + public: + FixUpdateDirectoryPermsRunnable(const char* aName, + bool aUseServiceOnFailure, + const nsAutoString& aInstallPath) + : Runnable(aName), + mState(State::Initializing) +# ifdef MOZ_MAINTENANCE_SERVICE + , + mUseServiceOnFailure(aUseServiceOnFailure) +# endif + { + size_t installPathSize = aInstallPath.Length() + 1; + mInstallPath = mozilla::MakeUnique<wchar_t[]>(installPathSize); + if (mInstallPath) { + HRESULT hrv = StringCchCopyW(mInstallPath.get(), installPathSize, + PromiseFlatString(aInstallPath).get()); + if (FAILED(hrv)) { + mInstallPath.reset(); + } + } + } + + NS_IMETHOD Run() override { +# ifdef MOZ_MAINTENANCE_SERVICE + // These constants control how often and how many times we poll the + // maintenance service to see if it has stopped. If there is no delay in + // the event queue, this works out to 8 minutes of polling. + const unsigned int kMaxQueries = 2400; + const unsigned int kQueryIntervalMS = 200; + // These constants control how often and how many times we attempt to + // start the service. If there is no delay in the event queue, this works + // out to 5 seconds of polling. + const unsigned int kMaxStartAttempts = 50; + const unsigned int kStartAttemptIntervalMS = 100; +# endif + + if (mState == State::Initializing) { + if (!mInstallPath) { + LOG( + ("Warning: No install path available in " + "FixUpdateDirectoryPermsRunnable\n")); + } + // In the event that the directory is owned by this user, we may be able + // to fix things without the maintenance service + mozilla::UniquePtr<wchar_t[]> updateDir; + HRESULT permResult = GetCommonUpdateDirectory( + mInstallPath.get(), SetPermissionsOf::AllFilesAndDirs, updateDir); + if (SUCCEEDED(permResult)) { + LOG(("Successfully fixed permissions from within Firefox\n")); + return NS_OK; + } +# ifdef MOZ_MAINTENANCE_SERVICE + else if (!mUseServiceOnFailure) { + LOG( + ("Error: Unable to fix permissions within Firefox and " + "maintenance service is disabled\n")); + return ReportUpdateError(); + } +# else + LOG(("Error: Unable to fix permissions\n")); + return ReportUpdateError(); +# endif + +# ifdef MOZ_MAINTENANCE_SERVICE + SC_HANDLE serviceManager = + OpenSCManager(nullptr, nullptr, + SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE); + mServiceManager.own(serviceManager); + if (!serviceManager) { + LOG( + ("Error: Unable to get the service manager. Cannot fix " + "permissions.\n")); + return NS_ERROR_FAILURE; + } + SC_HANDLE service = OpenServiceW(serviceManager, MAINTENANCE_SVC_NAME, + SERVICE_QUERY_STATUS | SERVICE_START); + mService.own(service); + if (!service) { + LOG( + ("Error: Unable to get the maintenance service. Unable fix " + "permissions without it.\n")); + return NS_ERROR_FAILURE; + } + + mStartServiceArgCount = mInstallPath ? 3 : 2; + mStartServiceArgs = + mozilla::MakeUnique<LPCWSTR[]>(mStartServiceArgCount); + if (!mStartServiceArgs) { + LOG( + ("Error: Unable to allocate memory for argument pointers. Cannot " + "fix permissions.\n")); + return NS_ERROR_FAILURE; + } + mStartServiceArgs[0] = L"MozillaMaintenance"; + mStartServiceArgs[1] = L"fix-update-directory-perms"; + if (mInstallPath) { + mStartServiceArgs[2] = mInstallPath.get(); + } + + mState = State::WaitingToStart; + mCurrentTry = 1; +# endif + } +# ifdef MOZ_MAINTENANCE_SERVICE + if (mState == State::WaitingToStart || + mState == State::WaitingForFinish) { + SERVICE_STATUS_PROCESS ssp; + DWORD bytesNeeded; + BOOL success = + QueryServiceStatusEx(mService, SC_STATUS_PROCESS_INFO, (LPBYTE)&ssp, + sizeof(SERVICE_STATUS_PROCESS), &bytesNeeded); + if (!success) { + DWORD lastError = GetLastError(); + // These 3 errors can occur when the service is not yet stopped but it + // is stopping. If we got another error, waiting will probably not + // help. + if (lastError != ERROR_INVALID_SERVICE_CONTROL && + lastError != ERROR_SERVICE_CANNOT_ACCEPT_CTRL && + lastError != ERROR_SERVICE_NOT_ACTIVE) { + LOG( + ("Error: Unable to query service when fixing permissions. Got " + "an error that cannot be fixed by waiting: 0x%lx\n", + lastError)); + return NS_ERROR_FAILURE; + } + if (mCurrentTry >= kMaxQueries) { + LOG( + ("Error: Unable to query service when fixing permissions: " + "Timed out after %u attempts.\n", + mCurrentTry)); + return NS_ERROR_FAILURE; + } + return RetryInMS(kQueryIntervalMS); + } else { // We successfully queried the service + if (ssp.dwCurrentState != SERVICE_STOPPED) { + return RetryInMS(kQueryIntervalMS); + } + if (mState == State::WaitingForFinish) { + if (ssp.dwWin32ExitCode != NO_ERROR) { + LOG( + ("Error: Maintenance Service was unable to fix update " + "directory permissions\n")); + return ReportUpdateError(); + } + LOG( + ("Maintenance service successully fixed update directory " + "permissions\n")); + return NS_OK; + } + mState = State::Starting; + mCurrentTry = 1; + } + } + if (mState == State::Starting) { + BOOL success = StartServiceW(mService, mStartServiceArgCount, + mStartServiceArgs.get()); + if (success) { + mState = State::WaitingForFinish; + mCurrentTry = 1; + return RetryInMS(kQueryIntervalMS); + } else if (mCurrentTry >= kMaxStartAttempts) { + LOG( + ("Error: Unable to fix permissions: Timed out after %u attempts " + "to start the maintenance service\n", + mCurrentTry)); + return NS_ERROR_FAILURE; + } + return RetryInMS(kStartAttemptIntervalMS); + } +# endif + // We should not have fallen through all three state checks above + LOG( + ("Error: Reached logically unreachable code when correcting update " + "directory permissions\n")); + return NS_ERROR_FAILURE; + } + + private: + State mState; + mozilla::UniquePtr<wchar_t[]> mInstallPath; +# ifdef MOZ_MAINTENANCE_SERVICE + bool mUseServiceOnFailure; + unsigned int mCurrentTry; + nsAutoServiceHandle mServiceManager; + nsAutoServiceHandle mService; + DWORD mStartServiceArgCount; + mozilla::UniquePtr<LPCWSTR[]> mStartServiceArgs; + + nsresult RetryInMS(unsigned int aDelayMS) { + ++mCurrentTry; + nsCOMPtr<nsIRunnable> runnable(this); + return NS_DelayedDispatchToCurrentThread(runnable.forget(), aDelayMS); + } +# endif + nsresult ReportUpdateError() { + return NS_DispatchToMainThread(NS_NewRunnableFunction( + "nsUpdateProcessor::FixUpdateDirectoryPerms::" + "FixUpdateDirectoryPermsRunnable::ReportUpdateError", + []() -> void { + nsCOMPtr<nsIObserverService> observerService = + services::GetObserverService(); + if (NS_WARN_IF(!observerService)) { + return; + } + observerService->NotifyObservers(nullptr, "update-error", + u"bad-perms"); + })); + } + }; + + nsCOMPtr<nsIProperties> dirSvc( + do_GetService("@mozilla.org/file/directory_service;1")); + NS_ENSURE_TRUE(dirSvc, NS_ERROR_FAILURE); + + nsCOMPtr<nsIFile> appPath; + nsresult rv = dirSvc->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile), + getter_AddRefs(appPath)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> installDir; + rv = appPath->GetParent(getter_AddRefs(installDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString installPath; + rv = installDir->GetPath(installPath); + NS_ENSURE_SUCCESS(rv, rv); + + // Stream transport service has a thread pool we can use so that this happens + // off the main thread. + nsCOMPtr<nsIEventTarget> eventTarget = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + NS_ENSURE_TRUE(eventTarget, NS_ERROR_FAILURE); + + nsCOMPtr<nsIRunnable> runnable = new FixUpdateDirectoryPermsRunnable( + "FixUpdateDirectoryPermsRunnable", aUseServiceOnFailure, installPath); + rv = eventTarget->Dispatch(runnable.forget()); + NS_ENSURE_SUCCESS(rv, rv); +#endif + return NS_OK; +} + +void nsUpdateProcessor::StartStagedUpdate() { + MOZ_ASSERT(!NS_IsMainThread(), "main thread"); + + nsresult rv = ProcessUpdates(mInfo.mGREDir, mInfo.mAppDir, mInfo.mUpdateRoot, + mInfo.mArgc, mInfo.mArgv, + mInfo.mAppVersion.get(), false, &mUpdaterPID); + NS_ENSURE_SUCCESS_VOID(rv); + + if (mUpdaterPID) { + // Track the state of the updater process while it is staging an update. + rv = NS_DispatchToCurrentThread( + NewRunnableMethod("nsUpdateProcessor::WaitForProcess", this, + &nsUpdateProcessor::WaitForProcess)); + NS_ENSURE_SUCCESS_VOID(rv); + } else { + // Failed to launch the updater process for some reason. + // We need to shutdown the current thread as there isn't anything more for + // us to do... + rv = NS_DispatchToMainThread( + NewRunnableMethod("nsUpdateProcessor::ShutdownWatcherThread", this, + &nsUpdateProcessor::ShutdownWatcherThread)); + NS_ENSURE_SUCCESS_VOID(rv); + } +} + +void nsUpdateProcessor::ShutdownWatcherThread() { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + mProcessWatcher->Shutdown(); + mProcessWatcher = nullptr; +} + +void nsUpdateProcessor::WaitForProcess() { + MOZ_ASSERT(!NS_IsMainThread(), "main thread"); + if (ProcessHasTerminated(mUpdaterPID)) { + NS_DispatchToMainThread(NewRunnableMethod( + "nsUpdateProcessor::UpdateDone", this, &nsUpdateProcessor::UpdateDone)); + } else { + NS_DispatchToCurrentThread( + NewRunnableMethod("nsUpdateProcessor::WaitForProcess", this, + &nsUpdateProcessor::WaitForProcess)); + } +} + +void nsUpdateProcessor::UpdateDone() { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + nsCOMPtr<nsIUpdateManager> um = + do_GetService("@mozilla.org/updates/update-manager;1"); + if (um) { + um->RefreshUpdateStatus(); + } + + ShutdownWatcherThread(); +} |