/* -*- 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 "nsDebug.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/ErrorNames.h"
#include "mozilla/Printf.h"
#include "mozilla/UniquePtr.h"
#include "nsIObserverService.h"
#include "nsNetCID.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Services.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/CmdLineAndEnvUtils.h"
#ifdef XP_MACOSX
# include
"nsILocalFileMac.h"
# include
"nsCommandLineServiceMac.h"
# include
"MacLaunchHelper.h"
# include
"updaterfileutils_osx.h"
# include
"mozilla/Monitor.h"
# include
"gfxPlatformMac.h"
#endif
#if defined(XP_WIN)
# include <direct.h>
# include <process.h>
# include <windows.h>
# include <shlwapi.h>
# include <strsafe.h>
# include <shellapi.h>
# include
"commonupdatedir.h"
# include
"nsWindowsHelpers.h"
# include
"pathhash.h"
# include
"WinUtils.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)
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, std::size(wpath))) {
return NS_ERROR_FAILURE;
}
CopyUTF16toUTF8(nsDependentString(wpath), aOutPath);
#else
char path[MAXPATHLEN];
if (!getcwd(path, std::size(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);
}
static void GetPidString(nsACString& output) {
output.Truncate(0);
output.AppendInt((int32_t)getpid());
}
/**
* Get the contents of the file when it 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.
* Otherwise we will loop infinitely and try to install it over and over.
*
* @param file
* The file object.
* @param buf
* The buffer holding the file contents.
*
* @return The result of `PR_Read`: number of characters read or -1 on error.
*/
template <size_t Size>
static int32_t ReadWritableFile(nsIFile* file,
char (&buf)[Size]) {
PRFileDesc* fd = nullptr;
nsresult rv = file->OpenNSPRFileDesc(PR_RDWR, 0660, &fd);
if (NS_FAILED(rv)) {
return 0;
}
const int32_t n = PR_Read(fd, buf, Size);
PR_Close(fd);
return n;
}
static nsresult WriteFile(nsIFile* file, nsACString& toWrite) {
PRFileDesc* fd = nullptr;
nsresult rv = file->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
0660, &fd);
NS_ENSURE_SUCCESS(rv, rv);
const int32_t n =
PR_Write(fd, PromiseFlatCString(toWrite).get(), toWrite.Length());
PR_Close(fd);
return (
unsigned long)n == toWrite.Length() ? NS_OK : NS_ERROR_FAILURE;
}
enum UpdateStatus {
eNoUpdateAction,
ePendingUpdate,
ePendingService,
ePendingElevate,
eAppliedUpdate,
eAppliedService,
};
/**
* 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)) {
// This buffer must be big enough to hold all valid status codes
char buf[32];
if (ReadWritableFile(statusFile, buf) >= 0) {
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;
}
nsresult GetUpdatePatchDir(nsIFile* updRootDir, nsIFile** updatesDirOut) {
nsresult rv;
nsCOMPtr<nsIFile> updatesDir;
rv = updRootDir->Clone(getter_AddRefs(updatesDir));
rv = updatesDir->AppendNative(
"updates"_ns);
NS_ENSURE_SUCCESS(rv, rv);
rv = updatesDir->AppendNative(
"0"_ns);
NS_ENSURE_SUCCESS(rv, rv);
updatesDir.forget(updatesDirOut);
return NS_OK;
}
nsresult IsMultiSessionInstallLockoutActive(nsIFile* updRootDir,
bool& isActive) {
nsresult rv;
nsCOMPtr<nsIFile> timestampFile;
rv = GetUpdatePatchDir(updRootDir, getter_AddRefs(timestampFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = timestampFile->AppendNative(
"update.timestamp"_ns);
NS_ENSURE_SUCCESS(rv, rv);
// Let's make sure we can hold any valid, unsigned 64 bit integer plus a null.
// Maximum 64 bit integer: 18446744073709551615 (20 characters)
const size_t bufferSize = 21;
char buffer[bufferSize];
int32_t readLen = ReadWritableFile(timestampFile, buffer);
NS_ENSURE_TRUE(readLen >= 0 && readLen <
static_cast<int32_t>(bufferSize),
NS_ERROR_FAILURE);
buffer[readLen] =
'\0';
// If we couldn't read anything from the file, the lockout is not active.
if (readLen == 0) {
isActive =
false;
return NS_OK;
}
nsDependentCString timestampString(buffer);
// This timestamp represents the end of the Multi Session Install Lockout.
uint64_t msilEnd = timestampString.ToInteger64(&rv);
NS_ENSURE_SUCCESS(rv, rv);
uint64_t now = PR_Now() / PR_USEC_PER_MSEC;
isActive = now < msilEnd;
#ifdef DEBUG
printf_stderr(
"Multi Session Install Lockout %s active\n",
isActive ?
"is" :
"is not");
#endif
return NS_OK;
}
nsresult WriteUpdateCompleteTestFile(nsIFile* updRootDir) {
nsCOMPtr<nsIFile> outputFile;
nsresult rv = updRootDir->Clone(getter_AddRefs(outputFile));
NS_ENSURE_SUCCESS(rv, rv);
outputFile->AppendNative(
"test_process_updates.txt"_ns);
nsAutoCString pid;
GetPidString(pid);
return WriteFile(outputFile, pid);
}
/**
* 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) {
MOZ_DIAGNOSTIC_ASSERT(
!restart || NS_IsMainThread(),
"restart may only be set when called on the main thread");
// 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.
#ifndef XP_MACOSX
nsAutoCString appFilePath;
#endif
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. Note that macOS uses the
// path to the application bundle, i.e. installDirPath, to relaunch the
// application.
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);
#elif !
defined(XP_MACOSX)
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;
}
#if defined(XP_MACOSX)
// If we're going to do a restart, we need to make sure the font registration
// thread has finished before this process exits (bug 1777332).
if (restart) {
gfxPlatformMac::WaitForFontRegistration();
}
// 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).
// We only check if we need elevation if we are restarting. We don't attempt
// to stage if elevation is required. Staging happens without the user knowing
// about it, and we don't want to ask for elevation for seemingly no reason.
bool needElevation =
false;
if (restart) {
needElevation = !IsRecursivelyWritable(installDirPath.get());
if (needElevation) {
// Normally we would check this via nsIAppStartup::wasSilentlyStarted,
// but nsIAppStartup isn't available yet.
char* mozAppSilentStart = PR_GetEnv(
"MOZ_APP_SILENT_START");
bool wasSilentlyStarted =
mozAppSilentStart && (strcmp(mozAppSilentStart,
"") != 0);
if (wasSilentlyStarted) {
// Elevation always requires prompting for credentials on macOS. If we
// are trying to restart silently, we must not display UI such as this
// prompt.
// We make this check here rather than in the updater, because it is
// actually Firefox that shows the elevation prompt (via
// InstallPrivilegedHelper), not the updater.
return;
}
}
}
#endif
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. This is
// `updateDir` for macOS and `appDir` for all other platforms. macOS cannot
// stage updates inside the .app bundle (`appDir`) without breaking the code
// signature on the bundle, so we use `updateDir` instead.
#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
GetPidString(pid);
#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();
#if defined(XP_MACOSX)
argv[6] = (
char*)installDirPath.get();
#else
argv[6] = (
char*)appFilePath.get();
#endif
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)
if (restart) {
// Ensure we've added URLs to load into the app command line if we're
// restarting.
CommandLineServiceMac::SetupMacCommandLine(argc, argv, restart);
if (needElevation) {
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);
}
}
#if !
defined(XP_WIN)
/**
* Wait briefly to see if a process terminates, then return true if it has.
*
* (Not implemented on Windows, where HandleWatcher is used instead.)
*/
static bool ProcessHasTerminated(ProcessType pt) {
# if 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
}
#endif
nsresult ProcessUpdates(nsIFile* greDir, nsIFile* appDir, nsIFile* updRootDir,
int argc,
char** argv,
const char* appVersion,
bool restart, ProcessType* pid) {
nsresult rv;
#ifdef XP_WIN
// If we're in a package, we know any updates that we find are not for us.
if (mozilla::widget::WinUtils::HasPackageIdentity()) {
return NS_OK;
}
#endif
nsCOMPtr<nsIFile> updatesDir;
rv = GetUpdatePatchDir(updRootDir, getter_AddRefs(updatesDir));
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) {}
#ifdef XP_WIN
nsUpdateProcessor::~nsUpdateProcessor() { mProcessWatcher.Stop(); }
#else
nsUpdateProcessor::~nsUpdateProcessor() =
default;
#endif
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
// worker 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(
"UpdateProcessor", getter_AddRefs(mWorkerThread), r);
}
void nsUpdateProcessor::StartStagedUpdate() {
MOZ_ASSERT(!NS_IsMainThread(),
"main thread");
// If we fail to launch the updater process or its monitor for some reason, we
// need to shut down the worker thread, as there isn't anything more for us to
// do.
auto onExitStopThread = mozilla::MakeScopeExit([&] {
nsresult rv = NS_DispatchToMainThread(
NewRunnableMethod(
"nsUpdateProcessor::ShutdownWorkerThread",
this,
&nsUpdateProcessor::ShutdownWorkerThread));
NS_ENSURE_SUCCESS_VOID(rv);
});
// Launch updater. (We do this on a worker thread to avoid blocking the main
// thread with file I/O.)
nsresult rv = ProcessUpdates(mInfo.mGREDir, mInfo.mAppDir, mInfo.mUpdateRoot,
mInfo.mArgc, mInfo.mArgv,
mInfo.mAppVersion.get(),
false, &mUpdaterPID);
if (NS_FAILED(rv)) {
MOZ_LOG(sUpdateLog, mozilla::LogLevel::Error,
(
"could not start updater process: %s", GetStaticErrorName(rv)));
return;
}
if (!mUpdaterPID) {
// not an error
MOZ_LOG(sUpdateLog, mozilla::LogLevel::Verbose,
(
"ProcessUpdates() indicated nothing to do"));
return;
}
#ifdef WIN32
// Set up a HandleWatcher to report to the main thread when we're done.
RefPtr<nsIThread> mainThread;
NS_GetMainThread(getter_AddRefs(mainThread));
mProcessWatcher.Watch(mUpdaterPID, mainThread,
NewRunnableMethod(
"nsUpdateProcessor::UpdateDone",
this,
&nsUpdateProcessor::UpdateDone));
// On Windows, that's all we need the worker thread for. Let
// `onExitStopThread` shut us down.
#else
// Monitor the state of the updater process while it is staging an update.
rv = NS_DispatchToCurrentThread(
NewRunnableMethod(
"nsUpdateProcessor::WaitForProcess",
this,
&nsUpdateProcessor::WaitForProcess));
if (NS_FAILED(rv)) {
MOZ_LOG(sUpdateLog, mozilla::LogLevel::Error,
(
"could not start updater process poll: error %s",
GetStaticErrorName(rv)));
return;
}
// Leave the worker thread alive to run WaitForProcess. Either it or its
// successors will be responsible for shutting down the worker thread.
onExitStopThread.release();
#endif
}
void nsUpdateProcessor::ShutdownWorkerThread() {
MOZ_ASSERT(NS_IsMainThread(),
"not main thread");
mWorkerThread->Shutdown();
mWorkerThread = nullptr;
}
#ifndef WIN32
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));
}
}
#endif
void nsUpdateProcessor::UpdateDone() {
MOZ_ASSERT(NS_IsMainThread(),
"not main thread");
nsCOMPtr<nsIUpdateManager> um =
do_GetService(
"@mozilla.org/updates/update-manager;1");
if (um) {
// This completes asynchronously, but nothing else that we are doing in this
// function requires waiting for this to complete.
RefPtr<mozilla::dom::Promise> outPromise;
um->RefreshUpdateStatus(getter_AddRefs(outPromise));
}
// On Windows, shutting down the worker thread is taken care of by another task.
// (Which may not have run yet, so we can't assert.)
#ifndef XP_WIN
ShutdownWorkerThread();
#endif
}
NS_IMETHODIMP
nsUpdateProcessor::GetServiceRegKeyExists(
bool* aResult) {
#ifndef XP_WIN
return NS_ERROR_NOT_IMPLEMENTED;
#else // #ifdef XP_WIN
nsCOMPtr<nsIProperties> dirSvc(
do_GetService(
"@mozilla.org/file/directory_service;1"));
NS_ENSURE_TRUE(dirSvc, NS_ERROR_SERVICE_NOT_AVAILABLE);
nsCOMPtr<nsIFile> installBin;
nsresult rv = dirSvc->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile),
getter_AddRefs(installBin));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> installDir;
rv = installBin->GetParent(getter_AddRefs(installDir));
NS_ENSURE_SUCCESS(rv, rv);
nsAutoString installPath;
rv = installDir->GetPath(installPath);
NS_ENSURE_SUCCESS(rv, rv);
wchar_t maintenanceServiceKey[MAX_PATH + 1];
BOOL success = CalculateRegistryPathFromFilePath(
PromiseFlatString(installPath).get(), maintenanceServiceKey);
NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
HKEY regHandle;
LSTATUS ls = RegOpenKeyExW(HKEY_LOCAL_MACHINE, maintenanceServiceKey, 0,
KEY_QUERY_VALUE | KEY_WOW64_64KEY, ®Handle);
if (ls == ERROR_SUCCESS) {
RegCloseKey(regHandle);
*aResult =
true;
return NS_OK;
}
if (ls == ERROR_FILE_NOT_FOUND) {
*aResult =
false;
return NS_OK;
}
// We got an error we weren't expecting reading the registry.
return NS_ERROR_NOT_AVAILABLE;
#endif // #ifdef XP_WIN
}
NS_IMETHODIMP
nsUpdateProcessor::AttemptAutomaticApplicationRestartWithLaunchArgs(
const nsTArray<nsString>& argvExtra, int32_t* pidRet) {
#ifndef XP_WIN
return NS_ERROR_NOT_IMPLEMENTED;
#else
// Retrieve current command line arguments for restart
// GetCommandLineW() returns a read only pointer to
// the arguments the process was launched with.
LPWSTR currentCommandLine = GetCommandLineW();
// Spawn a new process for the application based on the current
// command line with the -restart-pid <pid> flag. This flag
// can be used with MaybeWaitForProcessExit() to have
// the process wait until the parent process has exited.
if (currentCommandLine) {
// Append additional command line arguments to current command line for
// restart.
int currentArgc = 0;
UniquePtr<LPWSTR, LocalFreeDeleter> currentArgv(
CommandLineToArgvW(currentCommandLine, ¤tArgc));
nsTArray<
wchar_t*> restartCommandLineArgv(currentArgc + argvExtra.Length() +
2);
for (
int i = 0; i < currentArgc; i++) {
restartCommandLineArgv.AppendElement(currentArgv.get()[i]);
}
for (
const nsString& arg : argvExtra) {
restartCommandLineArgv.AppendElement(
static_cast<
wchar_t*>(arg.get()));
}
// Append -restart-pid flag and pid to restart command line.
DWORD pidCurrent = GetCurrentProcessId();
nsString pid;
pid.AppendInt(
static_cast<uint32_t>(pidCurrent));
nsString pidFlag = u
"-restart-pid"_ns;
restartCommandLineArgv.AppendElement(pidFlag.get());
restartCommandLineArgv.AppendElement(pid.get());
// Create new process that interacts with MaybeWaitForProcessExit()
// and sleeps until the original process is killed.
wchar_t exeName[MAX_PATH];
GetModuleFileNameW(NULL, exeName, MAX_PATH);
HANDLE childHandle;
WinLaunchChild(exeName, restartCommandLineArgv.Length(),
restartCommandLineArgv.Elements(), nullptr, &childHandle);
*pidRet = GetProcessId(childHandle);
CloseHandle(childHandle);
if (!*pidRet) {
printf_stderr(
"*** ApplyUpdate: !pidRet ***\n");
return NS_ERROR_ABORT;
}
printf_stderr(
"*** ApplyUpdate: launched pidRet = %d ***\n", *pidRet);
MOZ_LOG(sUpdateLog, mozilla::LogLevel::Debug,
(
"register application restart succeeded"));
}
else {
MOZ_LOG(sUpdateLog, mozilla::LogLevel::Error,
(
"could not register application restart"));
return NS_ERROR_NOT_AVAILABLE;
}
return NS_OK;
#endif // #ifndef XP_WIN
}
NS_IMETHODIMP
nsUpdateProcessor::WaitForProcessExit(uint32_t pid, uint32_t timeoutMS) {
#ifndef XP_WIN
return NS_ERROR_NOT_IMPLEMENTED;
#else
nsAutoHandle hProcess(OpenProcess(SYNCHRONIZE,
FALSE, pid));
if (!hProcess) {
// It's possible the pid is incorrect, or the process has exited.
// This isn't necessarily a failure state as if the process has
// already exited then that is the desired behavior.
MOZ_LOG(sUpdateLog, mozilla::LogLevel::Warning,
(
"WaitForProcessExit(%d): failed to OpenProcess", pid));
return NS_OK;
}
// Wait up to timeoutMS milliseconds for termination.
DWORD waitRv = WaitForSingleObjectEx(hProcess, timeoutMS,
FALSE);
if (waitRv != WAIT_OBJECT_0) {
if (waitRv == WAIT_TIMEOUT) {
MOZ_LOG(
sUpdateLog, mozilla::LogLevel::Debug,
(
"WaitForProcessExit(%d): timed out after %d MS", pid, timeoutMS));
return NS_ERROR_ABORT;
}
MOZ_LOG(sUpdateLog, mozilla::LogLevel::Warning,
(
"WaitForProcessExit(%d): unexpected error %lx", pid, waitRv));
return NS_ERROR_FAILURE;
}
MOZ_LOG(sUpdateLog, mozilla::LogLevel::Debug,
(
"WaitForProcessExit(%d): success", pid));
return NS_OK;
#endif // XP_WIN
}