gecko/toolkit/xre/nsUpdateDriver.cpp

532 lines
15 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is mozilla.org code.
*
* The Initial Developer of the Original Code is Google Inc.
* Portions created by the Initial Developer are Copyright (C) 2005
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Darin Fisher <darin@meer.net>
* Ben Turner <mozilla@songbirdnest.com>
* Robert Strong <robert.bugzilla@gmail.com>
* Josh Aas <josh@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#include <stdlib.h>
#include <stdio.h>
#include "nsUpdateDriver.h"
#include "nsXULAppAPI.h"
#include "nsAppRunner.h"
#include "nsILocalFile.h"
#include "nsCOMPtr.h"
#include "nsString.h"
#include "nsPrintfCString.h"
#include "prproces.h"
#include "prlog.h"
#include "prenv.h"
#include "nsVersionComparator.h"
#ifdef XP_MACOSX
#include "nsILocalFileMac.h"
#include "nsCommandLineServiceMac.h"
#include "MacLaunchHelper.h"
#endif
#if defined(XP_WIN)
# include <direct.h>
# include <process.h>
# include <windows.h>
# define getcwd(path, size) _getcwd(path, size)
# define getpid() GetCurrentProcessId()
#elif defined(XP_OS2)
# include <unistd.h>
# define INCL_DOSFILEMGR
# include <os2.h>
#elif defined(XP_UNIX) || defined(XP_BEOS)
# include <unistd.h>
#endif
//
// 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.
//
// 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 ;-)
//
// A similar #define lives in updater.cpp and should be kept in sync with this.
//
#if defined(XP_UNIX) && !defined(XP_MACOSX)
#define USE_EXECV
#endif
#ifdef PR_LOGGING
static PRLogModuleInfo *sUpdateLog = PR_NewLogModule("updatedriver");
#endif
#define LOG(args) PR_LOG(sUpdateLog, PR_LOG_DEBUG, args)
#ifdef XP_WIN
static const char kUpdaterBin[] = "updater.exe";
#else
static const char kUpdaterBin[] = "updater";
#endif
static const char kUpdaterINI[] = "updater.ini";
#ifdef XP_MACOSX
static const char kUpdaterApp[] = "updater.app";
#endif
#if defined(XP_UNIX) && !defined(XP_MACOSX)
static const char kUpdaterPNG[] = "updater.png";
#endif
static nsresult
GetCurrentWorkingDir(char *buf, size_t size)
{
// Cannot use NS_GetSpecialDirectory because XPCOM is not yet initialized.
// This code is duplicated from xpcom/io/SpecialSystemDirectory.cpp:
#if defined(XP_OS2)
if (DosQueryPathInfo( ".", FIL_QUERYFULLNAME, buf, size))
return NS_ERROR_FAILURE;
#elif defined(XP_WIN)
wchar_t wpath[MAX_PATH];
if (!_wgetcwd(wpath, size))
return NS_ERROR_FAILURE;
NS_ConvertUTF16toUTF8 path(wpath);
strncpy(buf, path.get(), size);
#else
if(!getcwd(buf, size))
return NS_ERROR_FAILURE;
#endif
return NS_OK;
}
#if defined(XP_MACOSX)
// This is a copy of OS X's XRE_GetBinaryPath from nsAppRunner.cpp with the
// gBinaryPath check removed so that the updater can reload the stub executable
// instead of xulrunner-bin. See bug 349737.
static nsresult
GetXULRunnerStubPath(const char* argv0, nsILocalFile* *aResult)
{
// Works even if we're not bundled.
CFBundleRef appBundle = ::CFBundleGetMainBundle();
if (!appBundle)
return NS_ERROR_FAILURE;
CFURLRef bundleURL = ::CFBundleCopyExecutableURL(appBundle);
if (!bundleURL)
return NS_ERROR_FAILURE;
nsCOMPtr<nsILocalFileMac> lfm;
nsresult rv = NS_NewLocalFileWithCFURL(bundleURL, PR_TRUE, getter_AddRefs(lfm));
::CFRelease(bundleURL);
if (NS_FAILED(rv))
return rv;
NS_ADDREF(*aResult = static_cast<nsILocalFile*>(lfm.get()));
return NS_OK;
}
#endif /* XP_MACOSX */
static PRBool
GetFile(nsIFile *dir, const nsCSubstring &name, nsCOMPtr<nsILocalFile> &result)
{
nsresult rv;
nsCOMPtr<nsIFile> file;
rv = dir->Clone(getter_AddRefs(file));
if (NS_FAILED(rv))
return PR_FALSE;
rv = file->AppendNative(name);
if (NS_FAILED(rv))
return PR_FALSE;
result = do_QueryInterface(file, &rv);
return NS_SUCCEEDED(rv);
}
static PRBool
GetStatusFile(nsIFile *dir, nsCOMPtr<nsILocalFile> &result)
{
return GetFile(dir, NS_LITERAL_CSTRING("update.status"), result);
}
static PRBool
IsPending(nsILocalFile *statusFile)
{
nsresult rv;
FILE *fp;
rv = statusFile->OpenANSIFileDesc("r", &fp);
if (NS_FAILED(rv))
return PR_FALSE;
char buf[32];
char *result = fgets(buf, sizeof(buf), fp);
fclose(fp);
if (!result)
return PR_FALSE;
const char kPending[] = "pending";
return (strncmp(buf, kPending, sizeof(kPending) - 1) == 0);
}
static PRBool
SetStatus(nsILocalFile *statusFile, const char *status)
{
FILE *fp;
nsresult rv = statusFile->OpenANSIFileDesc("w", &fp);
if (NS_FAILED(rv))
return PR_FALSE;
fprintf(fp, "%s\n", status);
fclose(fp);
return PR_TRUE;
}
static PRBool
GetVersionFile(nsIFile *dir, nsCOMPtr<nsILocalFile> &result)
{
return GetFile(dir, NS_LITERAL_CSTRING("update.version"), result);
}
// Compares the current application version with the update's application
// version.
static PRBool
IsOlderVersion(nsILocalFile *versionFile, const char *&appVersion)
{
nsresult rv;
FILE *fp;
rv = versionFile->OpenANSIFileDesc("r", &fp);
if (NS_FAILED(rv))
return PR_TRUE;
char buf[32];
char *result = fgets(buf, sizeof(buf), fp);
fclose(fp);
if (!result)
return PR_TRUE;
// Trim off any trailing newline
int len = strlen(result);
if (len > 0 && result[len - 1] == '\n')
result[len - 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 PR_FALSE;
if (NS_CompareVersions(appVersion, result) > 0)
return PR_TRUE;
return PR_FALSE;
}
static PRBool
CopyFileIntoUpdateDir(nsIFile *parentDir, const char *leafName, nsIFile *updateDir)
{
nsDependentCString leaf(leafName);
nsCOMPtr<nsIFile> file;
// Make sure there is not an existing file in the target location.
nsresult rv = updateDir->Clone(getter_AddRefs(file));
if (NS_FAILED(rv))
return PR_FALSE;
rv = file->AppendNative(leaf);
if (NS_FAILED(rv))
return PR_FALSE;
file->Remove(PR_FALSE);
// Now, copy into the target location.
rv = parentDir->Clone(getter_AddRefs(file));
if (NS_FAILED(rv))
return PR_FALSE;
rv = file->AppendNative(leaf);
if (NS_FAILED(rv))
return PR_FALSE;
rv = file->CopyToNative(updateDir, EmptyCString());
if (NS_FAILED(rv))
return PR_FALSE;
return PR_TRUE;
}
static PRBool
CopyUpdaterIntoUpdateDir(nsIFile *greDir, nsIFile *appDir, nsIFile *updateDir,
nsCOMPtr<nsIFile> &updater)
{
// Copy the updater application from the GRE and the updater ini from the app
#if defined(XP_MACOSX)
if (!CopyFileIntoUpdateDir(greDir, kUpdaterApp, updateDir))
return PR_FALSE;
#else
if (!CopyFileIntoUpdateDir(greDir, kUpdaterBin, updateDir))
return PR_FALSE;
#endif
CopyFileIntoUpdateDir(appDir, kUpdaterINI, updateDir);
#if defined(XP_UNIX) && !defined(XP_MACOSX)
nsCOMPtr<nsIFile> iconDir;
appDir->Clone(getter_AddRefs(iconDir));
iconDir->AppendNative(NS_LITERAL_CSTRING("icons"));
if (!CopyFileIntoUpdateDir(iconDir, kUpdaterPNG, updateDir))
return PR_FALSE;
#endif
// Finally, return the location of the updater binary.
nsresult rv = updateDir->Clone(getter_AddRefs(updater));
if (NS_FAILED(rv))
return PR_FALSE;
#if defined(XP_MACOSX)
rv = updater->AppendNative(NS_LITERAL_CSTRING(kUpdaterApp));
rv |= updater->AppendNative(NS_LITERAL_CSTRING("Contents"));
rv |= updater->AppendNative(NS_LITERAL_CSTRING("MacOS"));
if (NS_FAILED(rv))
return PR_FALSE;
#endif
rv = updater->AppendNative(NS_LITERAL_CSTRING(kUpdaterBin));
return NS_SUCCEEDED(rv);
}
static void
ApplyUpdate(nsIFile *greDir, nsIFile *updateDir, nsILocalFile *statusFile,
nsIFile *appDir, int appArgc, char **appArgv)
{
nsresult rv;
// Steps:
// - mark update as 'applying'
// - copy updater into update dir
// - run updater w/ appDir as the current working dir
nsCOMPtr<nsIFile> updater;
if (!CopyUpdaterIntoUpdateDir(greDir, appDir, updateDir, updater)) {
LOG(("failed copying updater\n"));
return;
}
// We need to use the value returned from XRE_GetBinaryPath when attempting
// to restart the running application.
nsCOMPtr<nsILocalFile> appFile;
#if defined(XP_MACOSX)
// On OS X we need to pass the location of the xulrunner-stub executable
// rather than xulrunner-bin. See bug 349737.
GetXULRunnerStubPath(appArgv[0], getter_AddRefs(appFile));
#else
XRE_GetBinaryPath(appArgv[0], getter_AddRefs(appFile));
#endif
if (!appFile)
return;
#ifdef XP_WIN
nsAutoString appFilePathW;
rv = appFile->GetPath(appFilePathW);
if (NS_FAILED(rv))
return;
NS_ConvertUTF16toUTF8 appFilePath(appFilePathW);
nsAutoString updaterPathW;
rv = updater->GetPath(updaterPathW);
if (NS_FAILED(rv))
return;
NS_ConvertUTF16toUTF8 updaterPath(updaterPathW);
#else
nsCAutoString appFilePath;
rv = appFile->GetNativePath(appFilePath);
if (NS_FAILED(rv))
return;
nsCAutoString updaterPath;
rv = updater->GetNativePath(updaterPath);
if (NS_FAILED(rv))
return;
#endif
// Get the directory to which the update will be applied. On Mac OSX we need
// to apply the update to the Foo.app directory which is the parent of the
// parent of the appDir. On other platforms we will just apply to the appDir.
#if defined(XP_MACOSX)
nsCAutoString applyToDir;
{
nsCOMPtr<nsIFile> parentDir1, parentDir2;
rv = appDir->GetParent(getter_AddRefs(parentDir1));
if (NS_FAILED(rv))
return;
rv = parentDir1->GetParent(getter_AddRefs(parentDir2));
if (NS_FAILED(rv))
return;
rv = parentDir2->GetNativePath(applyToDir);
}
#elif defined(XP_WIN)
nsAutoString applyToDir;
rv = appDir->GetPath(applyToDir);
#else
nsCAutoString applyToDir;
rv = appDir->GetNativePath(applyToDir);
#endif
if (NS_FAILED(rv))
return;
#if defined(XP_WIN)
nsAutoString updateDirPathW;
rv = updateDir->GetPath(updateDirPathW);
NS_ConvertUTF16toUTF8 updateDirPath(updateDirPathW);
#else
nsCAutoString updateDirPath;
rv = updateDir->GetNativePath(updateDirPath);
#endif
if (NS_FAILED(rv))
return;
// Get the current working directory.
char workingDirPath[MAXPATHLEN];
rv = GetCurrentWorkingDir(workingDirPath, sizeof(workingDirPath));
if (NS_FAILED(rv))
return;
if (!SetStatus(statusFile, "applying")) {
LOG(("failed setting status to 'applying'\n"));
return;
}
// Construct the PID argument for this process. If we are using execv, then
// we pass "0" which is then ignored by the updater.
#if defined(USE_EXECV)
NS_NAMED_LITERAL_CSTRING(pid, "0");
#else
nsCAutoString pid;
pid.AppendInt((PRInt32) getpid());
#endif
int argc = appArgc + 5;
char **argv = new char*[argc + 1];
if (!argv)
return;
argv[0] = (char*) updaterPath.get();
argv[1] = (char*) updateDirPath.get();
argv[2] = (char*) applyToDir.get();
argv[3] = (char*) pid.get();
if (appArgc) {
argv[4] = workingDirPath;
argv[5] = (char*) appFilePath.get();
for (int i = 1; i < appArgc; ++i)
argv[5 + i] = appArgv[i];
argc = 5 + appArgc;
argv[argc] = NULL;
} else {
argc = 4;
argv[4] = NULL;
}
if (gSafeMode) {
PR_SetEnv("MOZ_SAFE_MODE_RESTART=1");
}
LOG(("spawning updater process [%s]\n", updaterPath.get()));
#if defined(USE_EXECV)
execv(updaterPath.get(), argv);
#elif defined(XP_WIN)
if (!WinLaunchChild(updaterPathW.get(), argc, argv))
return;
_exit(0);
#elif defined(XP_MACOSX)
CommandLineServiceMac::SetupMacCommandLine(argc, argv, PR_TRUE);
// LaunchChildMac uses posix_spawnp and prefers the current
// architecture when launching. It doesn't require a
// null-terminated string but it doesn't matter if we pass one.
LaunchChildMac(argc, argv);
exit(0);
#else
PR_CreateProcessDetached(updaterPath.get(), argv, NULL, NULL);
exit(0);
#endif
}
nsresult
ProcessUpdates(nsIFile *greDir, nsIFile *appDir, nsIFile *updRootDir,
int argc, char **argv, const char *&appVersion)
{
nsresult rv;
nsCOMPtr<nsIFile> updatesDir;
rv = updRootDir->Clone(getter_AddRefs(updatesDir));
if (NS_FAILED(rv))
return rv;
rv = updatesDir->AppendNative(NS_LITERAL_CSTRING("updates"));
if (NS_FAILED(rv))
return rv;
rv = updatesDir->AppendNative(NS_LITERAL_CSTRING("0"));
if (NS_FAILED(rv))
return rv;
PRBool exists;
rv = updatesDir->Exists(&exists);
if (NS_FAILED(rv) || !exists)
return rv;
nsCOMPtr<nsILocalFile> statusFile;
if (GetStatusFile(updatesDir, statusFile) && IsPending(statusFile)) {
nsCOMPtr<nsILocalFile> versionFile;
// Remove the update if the update application version file doesn't exist
// or if the update's application version is less than the current
// application version.
if (!GetVersionFile(updatesDir, versionFile) ||
IsOlderVersion(versionFile, appVersion)) {
updatesDir->Remove(PR_TRUE);
} else {
ApplyUpdate(greDir, updatesDir, statusFile, appDir, argc, argv);
}
}
return NS_OK;
}