The test added in this stack doesn't really do anything new, but it does do some things repeatedly that had previously only been done once per test. This patch is largely aimed at making that functionality more self contained so that it doesn't break or work inconsistently when repeated. The biggest of these changes is to the `--test-process-updates` behavior. Previously, it would exit the browser after all update processing is complete. Now it additionally writes its PID into the update directory upon completion. This makes it more reasonable to make `runUpdateUsingApp` self contained, as there were problems where parts of the app were still running after this function exited, causing problems. Differential Revision: https://phabricator.services.mozilla.com/D231898
1117 lines
35 KiB
C++
1117 lines
35 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: */
|
|
/* 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 = 7;
|
|
if (restart) {
|
|
argc += 1; // callback working directory
|
|
argc += appArgc;
|
|
if (gRestartedByOS) {
|
|
argc += 1;
|
|
}
|
|
}
|
|
char** argv = static_cast<char**>(malloc((argc + 1) * sizeof(char*)));
|
|
if (!argv) {
|
|
return;
|
|
}
|
|
argv[0] = (char*)updaterPath.get();
|
|
argv[1] = const_cast<char*>("3");
|
|
argv[2] = (char*)updateDirPath.get();
|
|
argv[3] = (char*)installDirPath.get();
|
|
argv[4] = (char*)applyToDirPath.get();
|
|
argv[5] = const_cast<char*>("first");
|
|
argv[6] = (char*)pid.get();
|
|
if (restart && appArgc) {
|
|
argv[7] = (char*)workingDirPath.get();
|
|
#if defined(XP_MACOSX)
|
|
argv[8] = (char*)installDirPath.get();
|
|
#else
|
|
argv[8] = (char*)appFilePath.get();
|
|
#endif
|
|
for (int i = 1; i < appArgc; ++i) {
|
|
argv[8 + i] = appArgv[i];
|
|
}
|
|
if (gRestartedByOS) {
|
|
// We haven't truly started up, restore this argument so that we will have
|
|
// it upon restart.
|
|
argv[8 + 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
|
|
}
|