diff options
Diffstat (limited to 'toolkit/mozapps/update')
261 files changed, 35655 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/UpdateTelemetry.jsm b/toolkit/mozapps/update/UpdateTelemetry.jsm new file mode 100644 index 000000000..d64085143 --- /dev/null +++ b/toolkit/mozapps/update/UpdateTelemetry.jsm @@ -0,0 +1,488 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "AUSTLMY" +]; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm", this); + +this.AUSTLMY = { + // Telemetry for the application update background update check occurs when + // the background update timer fires after the update interval which is + // determined by the app.update.interval preference and its telemetry + // histogram IDs have the suffix '_NOTIFY'. + // Telemetry for the externally initiated background update check occurs when + // a call is made to |checkForBackgroundUpdates| which is typically initiated + // by an application when it has determined that the application should have + // received an update. This has separate telemetry so it is possible to + // analyze using the telemetry data systems that have not been updating when + // they should have. + + // The update check was performed by the call to checkForBackgroundUpdates in + // nsUpdateService.js. + EXTERNAL: "EXTERNAL", + // The update check was performed by the call to notify in nsUpdateService.js. + NOTIFY: "NOTIFY", + + /** + * Values for the UPDATE_CHECK_CODE_NOTIFY and UPDATE_CHECK_CODE_EXTERNAL + * Telemetry histograms. + */ + // No update found (no notification) + CHK_NO_UPDATE_FOUND: 0, + // Update will be downloaded in the background (background download) + CHK_DOWNLOAD_UPDATE: 1, + // Showing prompt due to the update.xml specifying showPrompt + // (update notification) + CHK_SHOWPROMPT_SNIPPET: 2, + // Showing prompt due to preference (update notification) + CHK_SHOWPROMPT_PREF: 3, + // Already has an active update in progress (no notification) + CHK_HAS_ACTIVEUPDATE: 8, + // A background download is already in progress (no notification) + CHK_IS_DOWNLOADING: 9, + // An update is already staged (no notification) + CHK_IS_STAGED: 10, + // An update is already downloaded (no notification) + CHK_IS_DOWNLOADED: 11, + // Background checks disabled by preference (no notification) + CHK_PREF_DISABLED: 12, + // Update checks disabled by admin locked preference (no notification) + CHK_ADMIN_DISABLED: 13, + // Unable to check for updates per hasUpdateMutex() (no notification) + CHK_NO_MUTEX: 14, + // Unable to check for updates per gCanCheckForUpdates (no notification). This + // should be covered by other codes and is recorded just in case. + CHK_UNABLE_TO_CHECK: 15, + // Background checks disabled for the current session (no notification) + CHK_DISABLED_FOR_SESSION: 16, + // Unable to perform a background check while offline (no notification) + CHK_OFFLINE: 17, + // Note: codes 18 - 21 were removed along with the certificate checking code. + // General update check failure and threshold reached + // (check failure notification) + CHK_GENERAL_ERROR_PROMPT: 22, + // General update check failure and threshold not reached (no notification) + CHK_GENERAL_ERROR_SILENT: 23, + // No compatible update found though there were updates (no notification) + CHK_NO_COMPAT_UPDATE_FOUND: 24, + // Update found for a previous version (no notification) + CHK_UPDATE_PREVIOUS_VERSION: 25, + // Update found for a version with the never preference set (no notification) + CHK_UPDATE_NEVER_PREF: 26, + // Update found without a type attribute (no notification) + CHK_UPDATE_INVALID_TYPE: 27, + // The system is no longer supported (system unsupported notification) + CHK_UNSUPPORTED: 28, + // Unable to apply updates (manual install to update notification) + CHK_UNABLE_TO_APPLY: 29, + // Unable to check for updates due to no OS version (no notification) + CHK_NO_OS_VERSION: 30, + // Unable to check for updates due to no OS ABI (no notification) + CHK_NO_OS_ABI: 31, + // Invalid url for app.update.url default preference (no notification) + CHK_INVALID_DEFAULT_URL: 32, + // Update elevation failures or cancelations threshold reached for this + // version, OSX only (no notification) + CHK_ELEVATION_DISABLED_FOR_VERSION: 35, + // User opted out of elevated updates for the available update version, OSX + // only (no notification) + CHK_ELEVATION_OPTOUT_FOR_VERSION: 36, + + /** + * Submit a telemetry ping for the update check result code or a telemetry + * ping for a count type histogram count when no update was found. The no + * update found ping is separate since it is the typical result, is less + * interesting than the other result codes, and it is easier to analyze the + * other codes without including it. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_CHECK_CODE_EXTERNAL + * UPDATE_CHECK_CODE_NOTIFY + * UPDATE_CHECK_NO_UPDATE_EXTERNAL + * UPDATE_CHECK_NO_UPDATE_NOTIFY + * @param aCode + * An integer value as defined by the values that start with CHK_ in + * the above section. + */ + pingCheckCode: function UT_pingCheckCode(aSuffix, aCode) { + try { + if (aCode == this.CHK_NO_UPDATE_FOUND) { + let id = "UPDATE_CHECK_NO_UPDATE_" + aSuffix; + // count type histogram + Services.telemetry.getHistogramById(id).add(); + } else { + let id = "UPDATE_CHECK_CODE_" + aSuffix; + // enumerated type histogram + Services.telemetry.getHistogramById(id).add(aCode); + } + } catch (e) { + Cu.reportError(e); + } + }, + + /** + * Submit a telemetry ping for a failed update check's unhandled error code + * when the pingCheckCode is CHK_GENERAL_ERROR_SILENT. The histogram is a + * keyed count type with key names that are prefixed with 'AUS_CHECK_EX_ERR_'. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_CHK_EXTENDED_ERROR_EXTERNAL + * UPDATE_CHK_EXTENDED_ERROR_NOTIFY + * @param aCode + * The extended error value return by a failed update check. + */ + pingCheckExError: function UT_pingCheckExError(aSuffix, aCode) { + try { + let id = "UPDATE_CHECK_EXTENDED_ERROR_" + aSuffix; + let val = "AUS_CHECK_EX_ERR_" + aCode; + // keyed count type histogram + Services.telemetry.getKeyedHistogramById(id).add(val); + } catch (e) { + Cu.reportError(e); + } + }, + + // The state code and if present the status error code were read on startup. + STARTUP: "STARTUP", + // The state code and status error code if present were read after staging. + STAGE: "STAGE", + + // Patch type Complete + PATCH_COMPLETE: "COMPLETE", + // Patch type partial + PATCH_PARTIAL: "PARTIAL", + // Patch type unknown + PATCH_UNKNOWN: "UNKNOWN", + + /** + * Values for the UPDATE_DOWNLOAD_CODE_COMPLETE and + * UPDATE_DOWNLOAD_CODE_PARTIAL Telemetry histograms. + */ + DWNLD_SUCCESS: 0, + DWNLD_RETRY_OFFLINE: 1, + DWNLD_RETRY_NET_TIMEOUT: 2, + DWNLD_RETRY_CONNECTION_REFUSED: 3, + DWNLD_RETRY_NET_RESET: 4, + DWNLD_ERR_NO_UPDATE: 5, + DWNLD_ERR_NO_UPDATE_PATCH: 6, + DWNLD_ERR_NO_PATCH_FILE: 7, + DWNLD_ERR_PATCH_SIZE_LARGER: 8, + DWNLD_ERR_PATCH_SIZE_NOT_EQUAL: 9, + DWNLD_ERR_BINDING_ABORTED: 10, + DWNLD_ERR_ABORT: 11, + DWNLD_ERR_DOCUMENT_NOT_CACHED: 12, + DWNLD_ERR_VERIFY_NO_REQUEST: 13, + DWNLD_ERR_VERIFY_PATCH_SIZE_NOT_EQUAL: 14, + DWNLD_ERR_VERIFY_NO_HASH_MATCH: 15, + + /** + * Submit a telemetry ping for the update download result code. + * + * @param aIsComplete + * If true the histogram is for a patch type complete, if false the + * histogram is for a patch type partial, and when undefined the + * histogram is for an unknown patch type. This is used to determine + * the histogram ID out of the following histogram IDs: + * UPDATE_DOWNLOAD_CODE_COMPLETE + * UPDATE_DOWNLOAD_CODE_PARTIAL + * @param aCode + * An integer value as defined by the values that start with DWNLD_ in + * the above section. + */ + pingDownloadCode: function UT_pingDownloadCode(aIsComplete, aCode) { + let patchType = this.PATCH_UNKNOWN; + if (aIsComplete === true) { + patchType = this.PATCH_COMPLETE; + } else if (aIsComplete === false) { + patchType = this.PATCH_PARTIAL; + } + try { + let id = "UPDATE_DOWNLOAD_CODE_" + patchType; + // enumerated type histogram + Services.telemetry.getHistogramById(id).add(aCode); + } catch (e) { + Cu.reportError(e); + } + }, + + /** + * Submit a telemetry ping for the update status state code. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_STATE_CODE_COMPLETE_STARTUP + * UPDATE_STATE_CODE_PARTIAL_STARTUP + * UPDATE_STATE_CODE_UNKNOWN_STARTUP + * UPDATE_STATE_CODE_COMPLETE_STAGE + * UPDATE_STATE_CODE_PARTIAL_STAGE + * UPDATE_STATE_CODE_UNKNOWN_STAGE + * @param aCode + * An integer value as defined by the values that start with STATE_ in + * the above section for the update state from the update.status file. + */ + pingStateCode: function UT_pingStateCode(aSuffix, aCode) { + try { + let id = "UPDATE_STATE_CODE_" + aSuffix; + // enumerated type histogram + Services.telemetry.getHistogramById(id).add(aCode); + } catch (e) { + Cu.reportError(e); + } + }, + + /** + * Submit a telemetry ping for the update status error code. This does not + * submit a success value which can be determined from the state code. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_STATUS_ERROR_CODE_COMPLETE_STARTUP + * UPDATE_STATUS_ERROR_CODE_PARTIAL_STARTUP + * UPDATE_STATUS_ERROR_CODE_UNKNOWN_STARTUP + * UPDATE_STATUS_ERROR_CODE_COMPLETE_STAGE + * UPDATE_STATUS_ERROR_CODE_PARTIAL_STAGE + * UPDATE_STATUS_ERROR_CODE_UNKNOWN_STAGE + * @param aCode + * An integer value for the error code from the update.status file. + */ + pingStatusErrorCode: function UT_pingStatusErrorCode(aSuffix, aCode) { + try { + let id = "UPDATE_STATUS_ERROR_CODE_" + aSuffix; + // enumerated type histogram + Services.telemetry.getHistogramById(id).add(aCode); + } catch (e) { + Cu.reportError(e); + } + }, + + /** + * Submit the interval in days since the last notification for this background + * update check or a boolean if the last notification is in the future. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_INVALID_LASTUPDATETIME_EXTERNAL + * UPDATE_INVALID_LASTUPDATETIME_NOTIFY + * UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL + * UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY + */ + pingLastUpdateTime: function UT_pingLastUpdateTime(aSuffix) { + const PREF_APP_UPDATE_LASTUPDATETIME = "app.update.lastUpdateTime.background-update-timer"; + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_LASTUPDATETIME)) { + let lastUpdateTimeSeconds = Services.prefs.getIntPref(PREF_APP_UPDATE_LASTUPDATETIME); + if (lastUpdateTimeSeconds) { + let currentTimeSeconds = Math.round(Date.now() / 1000); + if (lastUpdateTimeSeconds > currentTimeSeconds) { + try { + let id = "UPDATE_INVALID_LASTUPDATETIME_" + aSuffix; + // count type histogram + Services.telemetry.getHistogramById(id).add(); + } catch (e) { + Cu.reportError(e); + } + } else { + let intervalDays = (currentTimeSeconds - lastUpdateTimeSeconds) / + (60 * 60 * 24); + try { + let id = "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_" + aSuffix; + // exponential type histogram + Services.telemetry.getHistogramById(id).add(intervalDays); + } catch (e) { + Cu.reportError(e); + } + } + } + } + }, + + /** + * Submit a telemetry ping for the last page displayed by the update wizard. + * + * @param aPageID + * The page id for the last page displayed. + */ + pingWizLastPageCode: function UT_pingWizLastPageCode(aPageID) { + let pageMap = { invalid: 0, + dummy: 1, + checking: 2, + pluginupdatesfound: 3, + noupdatesfound: 4, + manualUpdate: 5, + unsupported: 6, + updatesfoundbasic: 8, + updatesfoundbillboard: 9, + downloading: 12, + errors: 13, + errorextra: 14, + errorpatching: 15, + finished: 16, + finishedBackground: 17, + installed: 18 }; + try { + let id = "UPDATE_WIZ_LAST_PAGE_CODE"; + // enumerated type histogram + Services.telemetry.getHistogramById(id).add(pageMap[aPageID] || + pageMap.invalid); + } catch (e) { + Cu.reportError(e); + } + }, + + /** + * Submit a telemetry ping for a boolean type histogram that indicates if the + * service is installed and a telemetry ping for a boolean type histogram that + * indicates if the service was at some point installed and is now + * uninstalled. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_SERVICE_INSTALLED_EXTERNAL + * UPDATE_SERVICE_INSTALLED_NOTIFY + * UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL + * UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY + * @param aInstalled + * Whether the service is installed. + */ + pingServiceInstallStatus: function UT_PSIS(aSuffix, aInstalled) { + // Report the error but don't throw since it is more important to + // successfully update than to throw. + if (!("@mozilla.org/windows-registry-key;1" in Cc)) { + Cu.reportError(Cr.NS_ERROR_NOT_AVAILABLE); + return; + } + + try { + let id = "UPDATE_SERVICE_INSTALLED_" + aSuffix; + // boolean type histogram + Services.telemetry.getHistogramById(id).add(aInstalled); + } catch (e) { + Cu.reportError(e); + } + + let attempted = 0; + try { + let wrk = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + wrk.open(wrk.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\MaintenanceService", + wrk.ACCESS_READ | wrk.WOW64_64); + // Was the service at some point installed, but is now uninstalled? + attempted = wrk.readIntValue("Attempted"); + wrk.close(); + } catch (e) { + // Since this will throw if the registry key doesn't exist (e.g. the + // service has never been installed) don't report an error. + } + + try { + let id = "UPDATE_SERVICE_MANUALLY_UNINSTALLED_" + aSuffix; + if (!aInstalled && attempted) { + // count type histogram + Services.telemetry.getHistogramById(id).add(); + } + } catch (e) { + Cu.reportError(e); + } + }, + + /** + * Submit a telemetry ping for a count type histogram when the expected value + * does not equal the boolean value of a pref or if the pref isn't present + * when the expected value does not equal default value. This lessens the + * amount of data submitted to telemetry. + * + * @param aID + * The histogram ID to report to. + * @param aPref + * The preference to check. + * @param aDefault + * The default value when the preference isn't present. + * @param aExpected (optional) + * If specified and the value is the same as the value that will be + * added the value won't be added to telemetry. + */ + pingBoolPref: function UT_pingBoolPref(aID, aPref, aDefault, aExpected) { + try { + let val = aDefault; + if (Services.prefs.getPrefType(aPref) != Ci.nsIPrefBranch.PREF_INVALID) { + val = Services.prefs.getBoolPref(aPref); + } + if (val != aExpected) { + // count type histogram + Services.telemetry.getHistogramById(aID).add(); + } + } catch (e) { + Cu.reportError(e); + } + }, + + /** + * Submit a telemetry ping for a histogram with the integer value of a + * preference when it is not the expected value or the default value when it + * is not the expected value. This lessens the amount of data submitted to + * telemetry. + * + * @param aID + * The histogram ID to report to. + * @param aPref + * The preference to check. + * @param aDefault + * The default value when the pref is not set. + * @param aExpected (optional) + * If specified and the value is the same as the value that will be + * added the value won't be added to telemetry. + */ + pingIntPref: function UT_pingIntPref(aID, aPref, aDefault, aExpected) { + try { + let val = aDefault; + if (Services.prefs.getPrefType(aPref) != Ci.nsIPrefBranch.PREF_INVALID) { + val = Services.prefs.getIntPref(aPref); + } + if (aExpected === undefined || val != aExpected) { + // enumerated or exponential type histogram + Services.telemetry.getHistogramById(aID).add(val); + } + } catch (e) { + Cu.reportError(e); + } + }, + + /** + * Submit a telemetry ping for all histogram types that take a single + * parameter to the telemetry add function and the count type histogram when + * the aExpected parameter is specified. If the aExpected parameter is + * specified and it equals the value specified by the aValue + * parameter the telemetry submission will be skipped. + * + * @param aID + * The histogram ID to report to. + * @param aValue + * The value to add when aExpected is not defined or the value to + * check if it is equal to when aExpected is defined. + * @param aExpected (optional) + * If specified and the value is the same as the value specified by + * aValue parameter the submission will be skipped. + */ + pingGeneric: function UT_pingGeneric(aID, aValue, aExpected) { + try { + if (aExpected === undefined) { + Services.telemetry.getHistogramById(aID).add(aValue); + } else if (aValue != aExpected) { + // count type histogram + Services.telemetry.getHistogramById(aID).add(); + } + } catch (e) { + Cu.reportError(e); + } + } +}; +Object.freeze(AUSTLMY); diff --git a/toolkit/mozapps/update/common-standalone/moz.build b/toolkit/mozapps/update/common-standalone/moz.build new file mode 100644 index 000000000..dbc787b43 --- /dev/null +++ b/toolkit/mozapps/update/common-standalone/moz.build @@ -0,0 +1,12 @@ +# 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/. + +Library('updatecommon-standalone') + +srcdir = '../common' + +include('../common/sources.mozbuild') + +if CONFIG['OS_ARCH'] == 'WINNT': + USE_STATIC_LIBS = True diff --git a/toolkit/mozapps/update/common/certificatecheck.cpp b/toolkit/mozapps/update/common/certificatecheck.cpp new file mode 100644 index 000000000..afb2e7ee3 --- /dev/null +++ b/toolkit/mozapps/update/common/certificatecheck.cpp @@ -0,0 +1,250 @@ +/* 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 <stdio.h> +#include <stdlib.h> +#include <windows.h> +#include <softpub.h> +#include <wintrust.h> + +#include "certificatecheck.h" +#include "updatecommon.h" + +static const int ENCODING = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; + +/** + * Checks to see if a file stored at filePath matches the specified info. + * + * @param filePath The PE file path to check + * @param infoToMatch The acceptable information to match + * @return ERROR_SUCCESS if successful, ERROR_NOT_FOUND if the info + * does not match, or the last error otherwise. + */ +DWORD +CheckCertificateForPEFile(LPCWSTR filePath, + CertificateCheckInfo &infoToMatch) +{ + HCERTSTORE certStore = nullptr; + HCRYPTMSG cryptMsg = nullptr; + PCCERT_CONTEXT certContext = nullptr; + PCMSG_SIGNER_INFO signerInfo = nullptr; + DWORD lastError = ERROR_SUCCESS; + + // Get the HCERTSTORE and HCRYPTMSG from the signed file. + DWORD encoding, contentType, formatType; + BOOL result = CryptQueryObject(CERT_QUERY_OBJECT_FILE, + filePath, + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, + CERT_QUERY_CONTENT_FLAG_ALL, + 0, &encoding, &contentType, + &formatType, &certStore, &cryptMsg, nullptr); + if (!result) { + lastError = GetLastError(); + LOG_WARN(("CryptQueryObject failed. (%d)", lastError)); + goto cleanup; + } + + // Pass in nullptr to get the needed signer information size. + DWORD signerInfoSize; + result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0, + nullptr, &signerInfoSize); + if (!result) { + lastError = GetLastError(); + LOG_WARN(("CryptMsgGetParam failed. (%d)", lastError)); + goto cleanup; + } + + // Allocate the needed size for the signer information. + signerInfo = (PCMSG_SIGNER_INFO)LocalAlloc(LPTR, signerInfoSize); + if (!signerInfo) { + lastError = GetLastError(); + LOG_WARN(("Unable to allocate memory for Signer Info. (%d)", lastError)); + goto cleanup; + } + + // Get the signer information (PCMSG_SIGNER_INFO). + // In particular we want the issuer and serial number. + result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0, + (PVOID)signerInfo, &signerInfoSize); + if (!result) { + lastError = GetLastError(); + LOG_WARN(("CryptMsgGetParam failed. (%d)", lastError)); + goto cleanup; + } + + // Search for the signer certificate in the certificate store. + CERT_INFO certInfo; + certInfo.Issuer = signerInfo->Issuer; + certInfo.SerialNumber = signerInfo->SerialNumber; + certContext = CertFindCertificateInStore(certStore, ENCODING, 0, + CERT_FIND_SUBJECT_CERT, + (PVOID)&certInfo, nullptr); + if (!certContext) { + lastError = GetLastError(); + LOG_WARN(("CertFindCertificateInStore failed. (%d)", lastError)); + goto cleanup; + } + + if (!DoCertificateAttributesMatch(certContext, infoToMatch)) { + lastError = ERROR_NOT_FOUND; + LOG_WARN(("Certificate did not match issuer or name. (%d)", lastError)); + goto cleanup; + } + +cleanup: + if (signerInfo) { + LocalFree(signerInfo); + } + if (certContext) { + CertFreeCertificateContext(certContext); + } + if (certStore) { + CertCloseStore(certStore, 0); + } + if (cryptMsg) { + CryptMsgClose(cryptMsg); + } + return lastError; +} + +/** + * Checks to see if a file stored at filePath matches the specified info. + * + * @param certContext The certificate context of the file + * @param infoToMatch The acceptable information to match + * @return FALSE if the info does not match or if any error occurs in the check + */ +BOOL +DoCertificateAttributesMatch(PCCERT_CONTEXT certContext, + CertificateCheckInfo &infoToMatch) +{ + DWORD dwData; + LPWSTR szName = nullptr; + + if (infoToMatch.issuer) { + // Pass in nullptr to get the needed size of the issuer buffer. + dwData = CertGetNameString(certContext, + CERT_NAME_SIMPLE_DISPLAY_TYPE, + CERT_NAME_ISSUER_FLAG, nullptr, + nullptr, 0); + + if (!dwData) { + LOG_WARN(("CertGetNameString failed. (%d)", GetLastError())); + return FALSE; + } + + // Allocate memory for Issuer name buffer. + szName = (LPWSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR)); + if (!szName) { + LOG_WARN(("Unable to allocate memory for issuer name. (%d)", + GetLastError())); + return FALSE; + } + + // Get Issuer name. + if (!CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, + CERT_NAME_ISSUER_FLAG, nullptr, szName, dwData)) { + LOG_WARN(("CertGetNameString failed. (%d)", GetLastError())); + LocalFree(szName); + return FALSE; + } + + // If the issuer does not match, return a failure. + if (!infoToMatch.issuer || + wcscmp(szName, infoToMatch.issuer)) { + LocalFree(szName); + return FALSE; + } + + LocalFree(szName); + szName = nullptr; + } + + if (infoToMatch.name) { + // Pass in nullptr to get the needed size of the name buffer. + dwData = CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, + 0, nullptr, nullptr, 0); + if (!dwData) { + LOG_WARN(("CertGetNameString failed. (%d)", GetLastError())); + return FALSE; + } + + // Allocate memory for the name buffer. + szName = (LPWSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR)); + if (!szName) { + LOG_WARN(("Unable to allocate memory for subject name. (%d)", + GetLastError())); + return FALSE; + } + + // Obtain the name. + if (!(CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, + nullptr, szName, dwData))) { + LOG_WARN(("CertGetNameString failed. (%d)", GetLastError())); + LocalFree(szName); + return FALSE; + } + + // If the issuer does not match, return a failure. + if (!infoToMatch.name || + wcscmp(szName, infoToMatch.name)) { + LocalFree(szName); + return FALSE; + } + + // We have a match! + LocalFree(szName); + } + + // If there were any errors we would have aborted by now. + return TRUE; +} + +/** + * Verifies the trust of the specified file path. + * + * @param filePath The file path to check. + * @return ERROR_SUCCESS if successful, or the last error code otherwise. + */ +DWORD +VerifyCertificateTrustForFile(LPCWSTR filePath) +{ + // Setup the file to check. + WINTRUST_FILE_INFO fileToCheck; + ZeroMemory(&fileToCheck, sizeof(fileToCheck)); + fileToCheck.cbStruct = sizeof(WINTRUST_FILE_INFO); + fileToCheck.pcwszFilePath = filePath; + + // Setup what to check, we want to check it is signed and trusted. + WINTRUST_DATA trustData; + ZeroMemory(&trustData, sizeof(trustData)); + trustData.cbStruct = sizeof(trustData); + trustData.pPolicyCallbackData = nullptr; + trustData.pSIPClientData = nullptr; + trustData.dwUIChoice = WTD_UI_NONE; + trustData.fdwRevocationChecks = WTD_REVOKE_NONE; + trustData.dwUnionChoice = WTD_CHOICE_FILE; + trustData.dwStateAction = 0; + trustData.hWVTStateData = nullptr; + trustData.pwszURLReference = nullptr; + // no UI + trustData.dwUIContext = 0; + trustData.pFile = &fileToCheck; + + GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2; + // Check if the file is signed by something that is trusted. + LONG ret = WinVerifyTrust(nullptr, &policyGUID, &trustData); + if (ERROR_SUCCESS == ret) { + // The hash that represents the subject is trusted and there were no + // verification errors. No publisher nor time stamp chain errors. + LOG(("The file \"%ls\" is signed and the signature was verified.", + filePath)); + return ERROR_SUCCESS; + } + + DWORD lastError = GetLastError(); + LOG_WARN(("There was an error validating trust of the certificate for file" + " \"%ls\". Returned: %d. (%d)", filePath, ret, lastError)); + return ret; +} diff --git a/toolkit/mozapps/update/common/certificatecheck.h b/toolkit/mozapps/update/common/certificatecheck.h new file mode 100644 index 000000000..43a7c85b6 --- /dev/null +++ b/toolkit/mozapps/update/common/certificatecheck.h @@ -0,0 +1,22 @@ +/* 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/. */ + +#ifndef _CERTIFICATECHECK_H_ +#define _CERTIFICATECHECK_H_ + +#include <wincrypt.h> + +struct CertificateCheckInfo +{ + LPCWSTR name; + LPCWSTR issuer; +}; + +BOOL DoCertificateAttributesMatch(PCCERT_CONTEXT pCertContext, + CertificateCheckInfo &infoToMatch); +DWORD VerifyCertificateTrustForFile(LPCWSTR filePath); +DWORD CheckCertificateForPEFile(LPCWSTR filePath, + CertificateCheckInfo &infoToMatch); + +#endif diff --git a/toolkit/mozapps/update/common/errors.h b/toolkit/mozapps/update/common/errors.h new file mode 100644 index 000000000..aac029175 --- /dev/null +++ b/toolkit/mozapps/update/common/errors.h @@ -0,0 +1,110 @@ +/* -*- 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/. */ + +#ifndef Errors_h__ +#define Errors_h__ + +#define OK 0 + +// Error codes that are no longer used should not be used again unless they +// aren't used in client code (e.g. nsUpdateService.js, updates.js, +// UpdatePrompt.js, etc.). + +#define MAR_ERROR_EMPTY_ACTION_LIST 1 +#define LOADSOURCE_ERROR_WRONG_SIZE 2 + +// Error codes 3-16 are for general update problems. +#define USAGE_ERROR 3 +#define CRC_ERROR 4 +#define PARSE_ERROR 5 +#define READ_ERROR 6 +#define WRITE_ERROR 7 +// #define UNEXPECTED_ERROR 8 // Replaced with errors 38-42 +#define ELEVATION_CANCELED 9 +#define READ_STRINGS_MEM_ERROR 10 +#define ARCHIVE_READER_MEM_ERROR 11 +#define BSPATCH_MEM_ERROR 12 +#define UPDATER_MEM_ERROR 13 +#define UPDATER_QUOTED_PATH_MEM_ERROR 14 +#define BAD_ACTION_ERROR 15 +#define STRING_CONVERSION_ERROR 16 + +// Error codes 17-23 are related to security tasks for MAR +// signing and MAR protection. +#define CERT_LOAD_ERROR 17 +#define CERT_HANDLING_ERROR 18 +#define CERT_VERIFY_ERROR 19 +#define ARCHIVE_NOT_OPEN 20 +#define COULD_NOT_READ_PRODUCT_INFO_BLOCK_ERROR 21 +#define MAR_CHANNEL_MISMATCH_ERROR 22 +#define VERSION_DOWNGRADE_ERROR 23 + +// Error codes 24-33 and 49-57 are for the Windows maintenance service. +#define SERVICE_UPDATER_COULD_NOT_BE_STARTED 24 +#define SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS 25 +#define SERVICE_UPDATER_SIGN_ERROR 26 +#define SERVICE_UPDATER_COMPARE_ERROR 27 +#define SERVICE_UPDATER_IDENTITY_ERROR 28 +#define SERVICE_STILL_APPLYING_ON_SUCCESS 29 +#define SERVICE_STILL_APPLYING_ON_FAILURE 30 +#define SERVICE_UPDATER_NOT_FIXED_DRIVE 31 +#define SERVICE_COULD_NOT_LOCK_UPDATER 32 +#define SERVICE_INSTALLDIR_ERROR 33 + +#define NO_INSTALLDIR_ERROR 34 +#define WRITE_ERROR_ACCESS_DENIED 35 +// #define WRITE_ERROR_SHARING_VIOLATION 36 // Replaced with errors 46-48 +#define WRITE_ERROR_CALLBACK_APP 37 +#define UNEXPECTED_BZIP_ERROR 39 +#define UNEXPECTED_MAR_ERROR 40 +#define UNEXPECTED_BSPATCH_ERROR 41 +#define UNEXPECTED_FILE_OPERATION_ERROR 42 +#define FILESYSTEM_MOUNT_READWRITE_ERROR 43 +#define DELETE_ERROR_EXPECTED_DIR 46 +#define DELETE_ERROR_EXPECTED_FILE 47 +#define RENAME_ERROR_EXPECTED_FILE 48 + +// Error codes 24-33 and 49-57 are for the Windows maintenance service. +#define SERVICE_COULD_NOT_COPY_UPDATER 49 +#define SERVICE_STILL_APPLYING_TERMINATED 50 +#define SERVICE_STILL_APPLYING_NO_EXIT_CODE 51 +#define SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR 52 +#define SERVICE_CALC_REG_PATH_ERROR 53 +#define SERVICE_INVALID_APPLYTO_DIR_ERROR 54 +#define SERVICE_INVALID_INSTALL_DIR_PATH_ERROR 55 +#define SERVICE_INVALID_WORKING_DIR_PATH_ERROR 56 +#define SERVICE_INSTALL_DIR_REG_ERROR 57 + +#define WRITE_ERROR_FILE_COPY 61 +#define WRITE_ERROR_DELETE_FILE 62 +#define WRITE_ERROR_OPEN_PATCH_FILE 63 +#define WRITE_ERROR_PATCH_FILE 64 +#define WRITE_ERROR_APPLY_DIR_PATH 65 +#define WRITE_ERROR_CALLBACK_PATH 66 +#define WRITE_ERROR_FILE_ACCESS_DENIED 67 +#define WRITE_ERROR_DIR_ACCESS_DENIED 68 +#define WRITE_ERROR_DELETE_BACKUP 69 +#define WRITE_ERROR_EXTRACT 70 +#define REMOVE_FILE_SPEC_ERROR 71 +#define INVALID_APPLYTO_DIR_STAGED_ERROR 72 +#define LOCK_ERROR_PATCH_FILE 73 +#define INVALID_APPLYTO_DIR_ERROR 74 +#define INVALID_INSTALL_DIR_PATH_ERROR 75 +#define INVALID_WORKING_DIR_PATH_ERROR 76 +#define INVALID_CALLBACK_PATH_ERROR 77 +#define INVALID_CALLBACK_DIR_ERROR 78 + +// Error codes 80 through 99 are reserved for nsUpdateService.js + +// The following error codes are only used by updater.exe +// when a fallback key exists for tests. +#define FALLBACKKEY_UNKNOWN_ERROR 100 +#define FALLBACKKEY_REGPATH_ERROR 101 +#define FALLBACKKEY_NOKEY_ERROR 102 +#define FALLBACKKEY_SERVICE_NO_STOP_ERROR 103 +#define FALLBACKKEY_LAUNCH_ERROR 104 + +#endif // Errors_h__ diff --git a/toolkit/mozapps/update/common/moz.build b/toolkit/mozapps/update/common/moz.build new file mode 100644 index 000000000..cacb0bad2 --- /dev/null +++ b/toolkit/mozapps/update/common/moz.build @@ -0,0 +1,32 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS += [ + 'readstrings.h', + 'updatecommon.h', + 'updatedefines.h', +] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': + EXPORTS += [ + 'pathhash.h', + 'uachelper.h', + 'updatehelper.cpp', + 'updatehelper.h', + ] + if CONFIG['MOZ_MAINTENANCE_SERVICE']: + EXPORTS += [ + 'certificatecheck.h', + 'registrycertificates.h', + ] + +Library('updatecommon') + +DEFINES['NS_NO_XPCOM'] = True + +srcdir = '.' + +include('sources.mozbuild') diff --git a/toolkit/mozapps/update/common/pathhash.cpp b/toolkit/mozapps/update/common/pathhash.cpp new file mode 100644 index 000000000..89a004cde --- /dev/null +++ b/toolkit/mozapps/update/common/pathhash.cpp @@ -0,0 +1,139 @@ +/* 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 <windows.h> +#include <wincrypt.h> +#include "pathhash.h" + + +/** + * Converts a binary sequence into a hex string + * + * @param hash The binary data sequence + * @param hashSize The size of the binary data sequence + * @param hexString A buffer to store the hex string, must be of + * size 2 * @hashSize +*/ +static void +BinaryDataToHexString(const BYTE *hash, DWORD &hashSize, + LPWSTR hexString) +{ + WCHAR *p = hexString; + for (DWORD i = 0; i < hashSize; ++i) { + wsprintfW(p, L"%.2x", hash[i]); + p += 2; + } +} + +/** + * Calculates an MD5 hash for the given input binary data + * + * @param data Any sequence of bytes + * @param dataSize The number of bytes inside @data + * @param hash Output buffer to store hash, must be freed by the caller + * @param hashSize The number of bytes in the output buffer + * @return TRUE on success +*/ +static BOOL +CalculateMD5(const char *data, DWORD dataSize, + BYTE **hash, DWORD &hashSize) +{ + HCRYPTPROV hProv = 0; + HCRYPTHASH hHash = 0; + + if (!CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_FULL, + CRYPT_VERIFYCONTEXT)) { + if (NTE_BAD_KEYSET != GetLastError()) { + return FALSE; + } + + // Maybe it doesn't exist, try to create it. + if (!CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_FULL, + CRYPT_VERIFYCONTEXT | CRYPT_NEWKEYSET)) { + return FALSE; + } + } + + if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) { + return FALSE; + } + + if (!CryptHashData(hHash, reinterpret_cast<const BYTE*>(data), + dataSize, 0)) { + return FALSE; + } + + DWORD dwCount = sizeof(DWORD); + if (!CryptGetHashParam(hHash, HP_HASHSIZE, (BYTE *)&hashSize, + &dwCount, 0)) { + return FALSE; + } + + *hash = new BYTE[hashSize]; + ZeroMemory(*hash, hashSize); + if (!CryptGetHashParam(hHash, HP_HASHVAL, *hash, &hashSize, 0)) { + return FALSE; + } + + if (hHash) { + CryptDestroyHash(hHash); + } + + if (hProv) { + CryptReleaseContext(hProv,0); + } + + return TRUE; +} + +/** + * Converts a file path into a unique registry location for cert storage + * + * @param filePath The input file path to get a registry path from + * @param registryPath A buffer to write the registry path to, must + * be of size in WCHARs MAX_PATH + 1 + * @return TRUE if successful +*/ +BOOL +CalculateRegistryPathFromFilePath(const LPCWSTR filePath, + LPWSTR registryPath) +{ + size_t filePathLen = wcslen(filePath); + if (!filePathLen) { + return FALSE; + } + + // If the file path ends in a slash, ignore that character + if (filePath[filePathLen -1] == L'\\' || + filePath[filePathLen - 1] == L'/') { + filePathLen--; + } + + // Copy in the full path into our own buffer. + // Copying in the extra slash is OK because we calculate the hash + // based on the filePathLen which excludes the slash. + // +2 to account for the possibly trailing slash and the null terminator. + WCHAR *lowercasePath = new WCHAR[filePathLen + 2]; + memset(lowercasePath, 0, (filePathLen + 2) * sizeof(WCHAR)); + wcsncpy(lowercasePath, filePath, filePathLen + 1); + _wcslwr(lowercasePath); + + BYTE *hash; + DWORD hashSize = 0; + if (!CalculateMD5(reinterpret_cast<const char*>(lowercasePath), + filePathLen * 2, + &hash, hashSize)) { + delete[] lowercasePath; + return FALSE; + } + delete[] lowercasePath; + + LPCWSTR baseRegPath = L"SOFTWARE\\Mozilla\\" + L"MaintenanceService\\"; + wcsncpy(registryPath, baseRegPath, MAX_PATH); + BinaryDataToHexString(hash, hashSize, + registryPath + wcslen(baseRegPath)); + delete[] hash; + return TRUE; +} diff --git a/toolkit/mozapps/update/common/pathhash.h b/toolkit/mozapps/update/common/pathhash.h new file mode 100644 index 000000000..a238317e1 --- /dev/null +++ b/toolkit/mozapps/update/common/pathhash.h @@ -0,0 +1,19 @@ +/* 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/. */ + +#ifndef _PATHHASH_H_ +#define _PATHHASH_H_ + +/** + * Converts a file path into a unique registry location for cert storage + * + * @param filePath The input file path to get a registry path from + * @param registryPath A buffer to write the registry path to, must + * be of size in WCHARs MAX_PATH + 1 + * @return TRUE if successful +*/ +BOOL CalculateRegistryPathFromFilePath(const LPCWSTR filePath, + LPWSTR registryPath); + +#endif diff --git a/toolkit/mozapps/update/common/readstrings.cpp b/toolkit/mozapps/update/common/readstrings.cpp new file mode 100644 index 000000000..428c91c0b --- /dev/null +++ b/toolkit/mozapps/update/common/readstrings.cpp @@ -0,0 +1,236 @@ +/* -*- 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 <limits.h> +#include <string.h> +#include <stdio.h> +#include "readstrings.h" +#include "errors.h" + +#ifdef XP_WIN +# define NS_tfopen _wfopen +# define OPEN_MODE L"rb" +#else +# define NS_tfopen fopen +# define OPEN_MODE "r" +#endif + +// stack based FILE wrapper to ensure that fclose is called. +class AutoFILE { +public: + explicit AutoFILE(FILE *fp) : fp_(fp) {} + ~AutoFILE() { if (fp_) fclose(fp_); } + operator FILE *() { return fp_; } +private: + FILE *fp_; +}; + +class AutoCharArray { +public: + explicit AutoCharArray(size_t len) { ptr_ = new char[len]; } + ~AutoCharArray() { delete[] ptr_; } + operator char *() { return ptr_; } +private: + char *ptr_; +}; + +static const char kNL[] = "\r\n"; +static const char kEquals[] = "="; +static const char kWhitespace[] = " \t"; +static const char kRBracket[] = "]"; + +static const char* +NS_strspnp(const char *delims, const char *str) +{ + const char *d; + do { + for (d = delims; *d != '\0'; ++d) { + if (*str == *d) { + ++str; + break; + } + } + } while (*d); + + return str; +} + +static char* +NS_strtok(const char *delims, char **str) +{ + if (!*str) + return nullptr; + + char *ret = (char*) NS_strspnp(delims, *str); + + if (!*ret) { + *str = ret; + return nullptr; + } + + char *i = ret; + do { + for (const char *d = delims; *d != '\0'; ++d) { + if (*i == *d) { + *i = '\0'; + *str = ++i; + return ret; + } + } + ++i; + } while (*i); + + *str = nullptr; + return ret; +} + +/** + * Find a key in a keyList containing zero-delimited keys ending with "\0\0". + * Returns a zero-based index of the key in the list, or -1 if the key is not found. + */ +static int +find_key(const char *keyList, char* key) +{ + if (!keyList) + return -1; + + int index = 0; + const char *p = keyList; + while (*p) + { + if (strcmp(key, p) == 0) + return index; + + p += strlen(p) + 1; + index++; + } + + // The key was not found if we came here + return -1; +} + +/** + * A very basic parser for updater.ini taken mostly from nsINIParser.cpp + * that can be used by standalone apps. + * + * @param path Path to the .ini file to read + * @param keyList List of zero-delimited keys ending with two zero characters + * @param numStrings Number of strings to read into results buffer - must be equal to the number of keys + * @param results Two-dimensional array of strings to be filled in the same order as the keys provided + * @param section Optional name of the section to read; defaults to "Strings" + */ +int +ReadStrings(const NS_tchar *path, + const char *keyList, + unsigned int numStrings, + char results[][MAX_TEXT_LEN], + const char *section) +{ + AutoFILE fp(NS_tfopen(path, OPEN_MODE)); + + if (!fp) + return READ_ERROR; + + /* get file size */ + if (fseek(fp, 0, SEEK_END) != 0) + return READ_ERROR; + + long len = ftell(fp); + if (len <= 0) + return READ_ERROR; + + size_t flen = size_t(len); + AutoCharArray fileContents(flen + 1); + if (!fileContents) + return READ_STRINGS_MEM_ERROR; + + /* read the file in one swoop */ + if (fseek(fp, 0, SEEK_SET) != 0) + return READ_ERROR; + + size_t rd = fread(fileContents, sizeof(char), flen, fp); + if (rd != flen) + return READ_ERROR; + + fileContents[flen] = '\0'; + + char *buffer = fileContents; + bool inStringsSection = false; + + unsigned int read = 0; + + while (char *token = NS_strtok(kNL, &buffer)) { + if (token[0] == '#' || token[0] == ';') // it's a comment + continue; + + token = (char*) NS_strspnp(kWhitespace, token); + if (!*token) // empty line + continue; + + if (token[0] == '[') { // section header! + ++token; + char const * currSection = token; + + char *rb = NS_strtok(kRBracket, &token); + if (!rb || NS_strtok(kWhitespace, &token)) { + // there's either an unclosed [Section or a [Section]Moretext! + // we could frankly decide that this INI file is malformed right + // here and stop, but we won't... keep going, looking for + // a well-formed [section] to continue working with + inStringsSection = false; + } + else { + if (section) + inStringsSection = strcmp(currSection, section) == 0; + else + inStringsSection = strcmp(currSection, "Strings") == 0; + } + + continue; + } + + if (!inStringsSection) { + // If we haven't found a section header (or we found a malformed + // section header), or this isn't the [Strings] section don't bother + // parsing this line. + continue; + } + + char *key = token; + char *e = NS_strtok(kEquals, &token); + if (!e) + continue; + + int keyIndex = find_key(keyList, key); + if (keyIndex >= 0 && (unsigned int)keyIndex < numStrings) + { + strncpy(results[keyIndex], token, MAX_TEXT_LEN - 1); + results[keyIndex][MAX_TEXT_LEN - 1] = '\0'; + read++; + } + } + + return (read == numStrings) ? OK : PARSE_ERROR; +} + +// A wrapper function to read strings for the updater. +// Added for compatibility with the original code. +int +ReadStrings(const NS_tchar *path, StringTable *results) +{ + const unsigned int kNumStrings = 2; + const char *kUpdaterKeys = "Title\0Info\0"; + char updater_strings[kNumStrings][MAX_TEXT_LEN]; + + int result = ReadStrings(path, kUpdaterKeys, kNumStrings, updater_strings); + + strncpy(results->title, updater_strings[0], MAX_TEXT_LEN - 1); + results->title[MAX_TEXT_LEN - 1] = '\0'; + strncpy(results->info, updater_strings[1], MAX_TEXT_LEN - 1); + results->info[MAX_TEXT_LEN - 1] = '\0'; + + return result; +} diff --git a/toolkit/mozapps/update/common/readstrings.h b/toolkit/mozapps/update/common/readstrings.h new file mode 100644 index 000000000..cd1909fc9 --- /dev/null +++ b/toolkit/mozapps/update/common/readstrings.h @@ -0,0 +1,43 @@ +/* -*- 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/. */ + +#ifndef READSTRINGS_H__ +#define READSTRINGS_H__ + +#define MAX_TEXT_LEN 600 + +#ifdef XP_WIN +# include <windows.h> + typedef WCHAR NS_tchar; +#else + typedef char NS_tchar; +#endif + +#ifndef NULL +#define NULL 0 +#endif + +struct StringTable +{ + char title[MAX_TEXT_LEN]; + char info[MAX_TEXT_LEN]; +}; + +/** + * This function reads in localized strings from updater.ini + */ +int ReadStrings(const NS_tchar *path, StringTable *results); + +/** + * This function reads in localized strings corresponding to the keys from a given .ini + */ +int ReadStrings(const NS_tchar *path, + const char *keyList, + unsigned int numStrings, + char results[][MAX_TEXT_LEN], + const char *section = nullptr); + +#endif // READSTRINGS_H__ diff --git a/toolkit/mozapps/update/common/registrycertificates.cpp b/toolkit/mozapps/update/common/registrycertificates.cpp new file mode 100644 index 000000000..1c9c44619 --- /dev/null +++ b/toolkit/mozapps/update/common/registrycertificates.cpp @@ -0,0 +1,154 @@ +/* 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 <stdio.h> +#include <stdlib.h> +#include <windows.h> + +#include "registrycertificates.h" +#include "pathhash.h" +#include "updatecommon.h" +#include "updatehelper.h" +#define MAX_KEY_LENGTH 255 + +/** + * Verifies if the file path matches any certificate stored in the registry. + * + * @param filePath The file path of the application to check if allowed. + * @param allowFallbackKeySkip when this is TRUE the fallback registry key will + * be used to skip the certificate check. This is the default since the + * fallback registry key is located under HKEY_LOCAL_MACHINE which can't be + * written to by a low integrity process. + * Note: the maintenance service binary can be used to perform this check for + * testing or troubleshooting. + * @return TRUE if the binary matches any of the allowed certificates. + */ +BOOL +DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate, LPCWSTR filePath, + BOOL allowFallbackKeySkip) +{ + WCHAR maintenanceServiceKey[MAX_PATH + 1]; + if (!CalculateRegistryPathFromFilePath(basePathForUpdate, + maintenanceServiceKey)) { + return FALSE; + } + + // We use KEY_WOW64_64KEY to always force 64-bit view. + // The user may have both x86 and x64 applications installed + // which each register information. We need a consistent place + // to put those certificate attributes in and hence why we always + // force the non redirected registry under Wow6432Node. + // This flag is ignored on 32bit systems. + HKEY baseKey; + LONG retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE, + maintenanceServiceKey, 0, + KEY_READ | KEY_WOW64_64KEY, &baseKey); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not open key. (%d)", retCode)); + // Our tests run with a different apply directory for each test. + // We use this registry key on our test slaves to store the + // allowed name/issuers. + retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE, + TEST_ONLY_FALLBACK_KEY_PATH, 0, + KEY_READ | KEY_WOW64_64KEY, &baseKey); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not open fallback key. (%d)", retCode)); + return FALSE; + } else if (allowFallbackKeySkip) { + LOG_WARN(("Fallback key present, skipping VerifyCertificateTrustForFile " + "check and the certificate attribute registry matching " + "check.")); + RegCloseKey(baseKey); + return TRUE; + } + } + + // Get the number of subkeys. + DWORD subkeyCount = 0; + retCode = RegQueryInfoKeyW(baseKey, nullptr, nullptr, nullptr, &subkeyCount, + nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not query info key. (%d)", retCode)); + RegCloseKey(baseKey); + return FALSE; + } + + // Enumerate the subkeys, each subkey represents an allowed certificate. + for (DWORD i = 0; i < subkeyCount; i++) { + WCHAR subkeyBuffer[MAX_KEY_LENGTH]; + DWORD subkeyBufferCount = MAX_KEY_LENGTH; + retCode = RegEnumKeyExW(baseKey, i, subkeyBuffer, + &subkeyBufferCount, nullptr, + nullptr, nullptr, nullptr); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not enum certs. (%d)", retCode)); + RegCloseKey(baseKey); + return FALSE; + } + + // Open the subkey for the current certificate + HKEY subKey; + retCode = RegOpenKeyExW(baseKey, + subkeyBuffer, + 0, + KEY_READ | KEY_WOW64_64KEY, + &subKey); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not open subkey. (%d)", retCode)); + continue; // Try the next subkey + } + + const int MAX_CHAR_COUNT = 256; + DWORD valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR); + WCHAR name[MAX_CHAR_COUNT] = { L'\0' }; + WCHAR issuer[MAX_CHAR_COUNT] = { L'\0' }; + + // Get the name from the registry + retCode = RegQueryValueExW(subKey, L"name", 0, nullptr, + (LPBYTE)name, &valueBufSize); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not obtain name from registry. (%d)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + // Get the issuer from the registry + valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR); + retCode = RegQueryValueExW(subKey, L"issuer", 0, nullptr, + (LPBYTE)issuer, &valueBufSize); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not obtain issuer from registry. (%d)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + CertificateCheckInfo allowedCertificate = { + name, + issuer, + }; + + retCode = CheckCertificateForPEFile(filePath, allowedCertificate); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Error on certificate check. (%d)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + retCode = VerifyCertificateTrustForFile(filePath); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Error on certificate trust check. (%d)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + RegCloseKey(baseKey); + // Raise the roof, we found a match! + return TRUE; + } + + RegCloseKey(baseKey); + // No certificates match, :'( + return FALSE; +} diff --git a/toolkit/mozapps/update/common/registrycertificates.h b/toolkit/mozapps/update/common/registrycertificates.h new file mode 100644 index 000000000..9f68d1a8d --- /dev/null +++ b/toolkit/mozapps/update/common/registrycertificates.h @@ -0,0 +1,14 @@ +/* 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/. */ + +#ifndef _REGISTRYCERTIFICATES_H_ +#define _REGISTRYCERTIFICATES_H_ + +#include "certificatecheck.h" + +BOOL DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate, + LPCWSTR filePath, + BOOL allowFallbackKeySkip = TRUE); + +#endif diff --git a/toolkit/mozapps/update/common/sources.mozbuild b/toolkit/mozapps/update/common/sources.mozbuild new file mode 100644 index 000000000..cde51e09b --- /dev/null +++ b/toolkit/mozapps/update/common/sources.mozbuild @@ -0,0 +1,28 @@ +# 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/. + +sources = [] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': + sources += [ + 'pathhash.cpp', + 'uachelper.cpp', + 'updatehelper.cpp', + ] + if CONFIG['MOZ_MAINTENANCE_SERVICE']: + sources += [ + 'certificatecheck.cpp', + 'registrycertificates.cpp', + ] + OS_LIBS += [ + 'crypt32', + 'wintrust', + ] + +sources += [ + 'readstrings.cpp', + 'updatecommon.cpp', +] + +SOURCES += sorted(['%s/%s' % (srcdir, s) for s in sources]) diff --git a/toolkit/mozapps/update/common/uachelper.cpp b/toolkit/mozapps/update/common/uachelper.cpp new file mode 100644 index 000000000..11647aa72 --- /dev/null +++ b/toolkit/mozapps/update/common/uachelper.cpp @@ -0,0 +1,222 @@ +/* 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 <windows.h> +#include <wtsapi32.h> +#include "uachelper.h" +#include "updatecommon.h" + +// See the MSDN documentation with title: Privilege Constants +// At the time of this writing, this documentation is located at: +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb530716%28v=vs.85%29.aspx +LPCTSTR UACHelper::PrivsToDisable[] = { + SE_ASSIGNPRIMARYTOKEN_NAME, + SE_AUDIT_NAME, + SE_BACKUP_NAME, + // CreateProcess will succeed but the app will fail to launch on some WinXP + // machines if SE_CHANGE_NOTIFY_NAME is disabled. In particular this happens + // for limited user accounts on those machines. The define is kept here as a + // reminder that it should never be re-added. + // This permission is for directory watching but also from MSDN: "This + // privilege also causes the system to skip all traversal access checks." + // SE_CHANGE_NOTIFY_NAME, + SE_CREATE_GLOBAL_NAME, + SE_CREATE_PAGEFILE_NAME, + SE_CREATE_PERMANENT_NAME, + SE_CREATE_SYMBOLIC_LINK_NAME, + SE_CREATE_TOKEN_NAME, + SE_DEBUG_NAME, + SE_ENABLE_DELEGATION_NAME, + SE_IMPERSONATE_NAME, + SE_INC_BASE_PRIORITY_NAME, + SE_INCREASE_QUOTA_NAME, + SE_INC_WORKING_SET_NAME, + SE_LOAD_DRIVER_NAME, + SE_LOCK_MEMORY_NAME, + SE_MACHINE_ACCOUNT_NAME, + SE_MANAGE_VOLUME_NAME, + SE_PROF_SINGLE_PROCESS_NAME, + SE_RELABEL_NAME, + SE_REMOTE_SHUTDOWN_NAME, + SE_RESTORE_NAME, + SE_SECURITY_NAME, + SE_SHUTDOWN_NAME, + SE_SYNC_AGENT_NAME, + SE_SYSTEM_ENVIRONMENT_NAME, + SE_SYSTEM_PROFILE_NAME, + SE_SYSTEMTIME_NAME, + SE_TAKE_OWNERSHIP_NAME, + SE_TCB_NAME, + SE_TIME_ZONE_NAME, + SE_TRUSTED_CREDMAN_ACCESS_NAME, + SE_UNDOCK_NAME, + SE_UNSOLICITED_INPUT_NAME +}; + +/** + * Opens a user token for the given session ID + * + * @param sessionID The session ID for the token to obtain + * @return A handle to the token to obtain which will be primary if enough + * permissions exist. Caller should close the handle. + */ +HANDLE +UACHelper::OpenUserToken(DWORD sessionID) +{ + HMODULE module = LoadLibraryW(L"wtsapi32.dll"); + HANDLE token = nullptr; + decltype(WTSQueryUserToken)* wtsQueryUserToken = + (decltype(WTSQueryUserToken)*) GetProcAddress(module, "WTSQueryUserToken"); + if (wtsQueryUserToken) { + wtsQueryUserToken(sessionID, &token); + } + FreeLibrary(module); + return token; +} + +/** + * Opens a linked token for the specified token. + * + * @param token The token to get the linked token from + * @return A linked token or nullptr if one does not exist. + * Caller should close the handle. + */ +HANDLE +UACHelper::OpenLinkedToken(HANDLE token) +{ + // Magic below... + // UAC creates 2 tokens. One is the restricted token which we have. + // the other is the UAC elevated one. Since we are running as a service + // as the system account we have access to both. + TOKEN_LINKED_TOKEN tlt; + HANDLE hNewLinkedToken = nullptr; + DWORD len; + if (GetTokenInformation(token, (TOKEN_INFORMATION_CLASS)TokenLinkedToken, + &tlt, sizeof(TOKEN_LINKED_TOKEN), &len)) { + token = tlt.LinkedToken; + hNewLinkedToken = token; + } + return hNewLinkedToken; +} + + +/** + * Enables or disables a privilege for the specified token. + * + * @param token The token to adjust the privilege on. + * @param priv The privilege to adjust. + * @param enable Whether to enable or disable it + * @return TRUE if the token was adjusted to the specified value. + */ +BOOL +UACHelper::SetPrivilege(HANDLE token, LPCTSTR priv, BOOL enable) +{ + LUID luidOfPriv; + if (!LookupPrivilegeValue(nullptr, priv, &luidOfPriv)) { + return FALSE; + } + + TOKEN_PRIVILEGES tokenPriv; + tokenPriv.PrivilegeCount = 1; + tokenPriv.Privileges[0].Luid = luidOfPriv; + tokenPriv.Privileges[0].Attributes = enable ? SE_PRIVILEGE_ENABLED : 0; + + SetLastError(ERROR_SUCCESS); + if (!AdjustTokenPrivileges(token, false, &tokenPriv, + sizeof(tokenPriv), nullptr, nullptr)) { + return FALSE; + } + + return GetLastError() == ERROR_SUCCESS; +} + +/** + * For each privilege that is specified, an attempt will be made to + * drop the privilege. + * + * @param token The token to adjust the privilege on. + * Pass nullptr for current token. + * @param unneededPrivs An array of unneeded privileges. + * @param count The size of the array + * @return TRUE if there were no errors + */ +BOOL +UACHelper::DisableUnneededPrivileges(HANDLE token, + LPCTSTR *unneededPrivs, + size_t count) +{ + HANDLE obtainedToken = nullptr; + if (!token) { + // Note: This handle is a pseudo-handle and need not be closed + HANDLE process = GetCurrentProcess(); + if (!OpenProcessToken(process, TOKEN_ALL_ACCESS_P, &obtainedToken)) { + LOG_WARN(("Could not obtain token for current process, no " + "privileges changed. (%d)", GetLastError())); + return FALSE; + } + token = obtainedToken; + } + + BOOL result = TRUE; + for (size_t i = 0; i < count; i++) { + if (SetPrivilege(token, unneededPrivs[i], FALSE)) { + LOG(("Disabled unneeded token privilege: %s.", + unneededPrivs[i])); + } else { + LOG(("Could not disable token privilege value: %s. (%d)", + unneededPrivs[i], GetLastError())); + result = FALSE; + } + } + + if (obtainedToken) { + CloseHandle(obtainedToken); + } + return result; +} + +/** + * Disables privileges for the specified token. + * The privileges to disable are in PrivsToDisable. + * In the future there could be new privs and we are not sure if we should + * explicitly disable these or not. + * + * @param token The token to drop the privilege on. + * Pass nullptr for current token. + * @return TRUE if there were no errors + */ +BOOL +UACHelper::DisablePrivileges(HANDLE token) +{ + static const size_t PrivsToDisableSize = + sizeof(UACHelper::PrivsToDisable) / sizeof(UACHelper::PrivsToDisable[0]); + + return DisableUnneededPrivileges(token, UACHelper::PrivsToDisable, + PrivsToDisableSize); +} + +/** + * Check if the current user can elevate. + * + * @return true if the user can elevate. + * false otherwise. + */ +bool +UACHelper::CanUserElevate() +{ + HANDLE token; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { + return false; + } + + TOKEN_ELEVATION_TYPE elevationType; + DWORD len; + bool canElevate = GetTokenInformation(token, TokenElevationType, + &elevationType, + sizeof(elevationType), &len) && + (elevationType == TokenElevationTypeLimited); + CloseHandle(token); + + return canElevate; +} diff --git a/toolkit/mozapps/update/common/uachelper.h b/toolkit/mozapps/update/common/uachelper.h new file mode 100644 index 000000000..6481ff5b5 --- /dev/null +++ b/toolkit/mozapps/update/common/uachelper.h @@ -0,0 +1,23 @@ +/* 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/. */ + +#ifndef _UACHELPER_H_ +#define _UACHELPER_H_ + +class UACHelper +{ +public: + static HANDLE OpenUserToken(DWORD sessionID); + static HANDLE OpenLinkedToken(HANDLE token); + static BOOL DisablePrivileges(HANDLE token); + static bool CanUserElevate(); + +private: + static BOOL SetPrivilege(HANDLE token, LPCTSTR privs, BOOL enable); + static BOOL DisableUnneededPrivileges(HANDLE token, + LPCTSTR *unneededPrivs, size_t count); + static LPCTSTR PrivsToDisable[]; +}; + +#endif diff --git a/toolkit/mozapps/update/common/updatecommon.cpp b/toolkit/mozapps/update/common/updatecommon.cpp new file mode 100644 index 000000000..aff7d7260 --- /dev/null +++ b/toolkit/mozapps/update/common/updatecommon.cpp @@ -0,0 +1,213 @@ +/* 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/. */ + +#if defined(XP_WIN) +#include <windows.h> +#endif + + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <stdarg.h> + +#include "updatecommon.h" + +UpdateLog::UpdateLog() : logFP(nullptr) +{ +} + +void UpdateLog::Init(NS_tchar* sourcePath, + const NS_tchar* fileName) +{ + if (logFP) { + return; + } + + int dstFilePathLen = NS_tsnprintf(mDstFilePath, + sizeof(mDstFilePath)/sizeof(mDstFilePath[0]), + NS_T("%s/%s"), sourcePath, fileName); + // If the destination path was over the length limit, + // disable logging by skipping opening the file and setting logFP. + if ((dstFilePathLen > 0) && + (dstFilePathLen < + static_cast<int>(sizeof(mDstFilePath)/sizeof(mDstFilePath[0])))) { +#ifdef XP_WIN + if (GetTempFileNameW(sourcePath, L"log", 0, mTmpFilePath) != 0) { + logFP = NS_tfopen(mTmpFilePath, NS_T("w")); + + // Delete this file now so it is possible to tell from the unelevated + // updater process if the elevated updater process has written the log. + DeleteFileW(mDstFilePath); + } +#elif XP_MACOSX + logFP = NS_tfopen(mDstFilePath, NS_T("w")); +#else + // On platforms that have an updates directory in the installation directory + // (e.g. platforms other than Windows and Mac) the update log is written to + // a temporary file and then to the update log file. This is needed since + // the installation directory is moved during a replace request. This can be + // removed when the platform's updates directory is located outside of the + // installation directory. + logFP = tmpfile(); +#endif + } +} + +void UpdateLog::Finish() +{ + if (!logFP) { + return; + } + +#if !defined(XP_WIN) && !defined(XP_MACOSX) + const int blockSize = 1024; + char buffer[blockSize]; + fflush(logFP); + rewind(logFP); + + FILE *updateLogFP = NS_tfopen(mDstFilePath, NS_T("wb+")); + while (!feof(logFP)) { + size_t read = fread(buffer, 1, blockSize, logFP); + if (ferror(logFP)) { + fclose(logFP); + logFP = nullptr; + fclose(updateLogFP); + updateLogFP = nullptr; + return; + } + + size_t written = 0; + + while (written < read) { + size_t chunkWritten = fwrite(buffer, 1, read - written, updateLogFP); + if (chunkWritten <= 0) { + fclose(logFP); + logFP = nullptr; + fclose(updateLogFP); + updateLogFP = nullptr; + return; + } + + written += chunkWritten; + } + } + fclose(updateLogFP); + updateLogFP = nullptr; +#endif + + fclose(logFP); + logFP = nullptr; + +#ifdef XP_WIN + // When the log file already exists then the elevated updater has already + // written the log file and the temp file for the log should be discarded. + if (!NS_taccess(mDstFilePath, F_OK)) { + DeleteFileW(mTmpFilePath); + } else { + MoveFileW(mTmpFilePath, mDstFilePath); + } +#endif +} + +void UpdateLog::Flush() +{ + if (!logFP) { + return; + } + + fflush(logFP); +} + +void UpdateLog::Printf(const char *fmt, ... ) +{ + if (!logFP) { + return; + } + + va_list ap; + va_start(ap, fmt); + vfprintf(logFP, fmt, ap); + fprintf(logFP, "\n"); + va_end(ap); +} + +void UpdateLog::WarnPrintf(const char *fmt, ... ) +{ + if (!logFP) { + return; + } + + va_list ap; + va_start(ap, fmt); + fprintf(logFP, "*** Warning: "); + vfprintf(logFP, fmt, ap); + fprintf(logFP, "***\n"); + va_end(ap); +} + +/** + * Performs checks of a full path for validity for this application. + * + * @param origFullPath + * The full path to check. + * @return true if the path is valid for this application and false otherwise. + */ +bool +IsValidFullPath(NS_tchar* origFullPath) +{ + // Subtract 1 from MAXPATHLEN for null termination. + if (NS_tstrlen(origFullPath) > MAXPATHLEN - 1) { + // The path is longer than acceptable for this application. + return false; + } + +#ifdef XP_WIN + NS_tchar testPath[MAXPATHLEN] = {NS_T('\0')}; + // GetFullPathNameW will replace / with \ which PathCanonicalizeW requires. + if (GetFullPathNameW(origFullPath, MAXPATHLEN, testPath, nullptr) == 0) { + // Unable to get the full name for the path (e.g. invalid path). + return false; + } + + NS_tchar canonicalPath[MAXPATHLEN] = {NS_T('\0')}; + if (!PathCanonicalizeW(canonicalPath, testPath)) { + // Path could not be canonicalized (e.g. invalid path). + return false; + } + + // Check if the path passed in resolves to a differerent path. + if (NS_tstricmp(origFullPath, canonicalPath) != 0) { + // Case insensitive string comparison between the supplied path and the + // canonical path are not equal. This will prevent directory traversal and + // the use of / in paths since they are converted to \. + return false; + } + + NS_tstrncpy(testPath, origFullPath, MAXPATHLEN); + if (!PathStripToRootW(testPath)) { + // It should always be possible to strip a valid path to its root. + return false; + } + + if (origFullPath[0] == NS_T('\\')) { + // Only allow UNC server share paths. + if (!PathIsUNCServerShareW(testPath)) { + return false; + } + } +#else + // Only allow full paths. + if (origFullPath[0] != NS_T('/')) { + return false; + } + + // The path must not traverse directories + if (NS_tstrstr(origFullPath, NS_T("..")) != nullptr || + NS_tstrstr(origFullPath, NS_T("./")) != nullptr) { + return false; + } +#endif + return true; +} diff --git a/toolkit/mozapps/update/common/updatecommon.h b/toolkit/mozapps/update/common/updatecommon.h new file mode 100644 index 000000000..3055851d8 --- /dev/null +++ b/toolkit/mozapps/update/common/updatecommon.h @@ -0,0 +1,47 @@ +/* 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/. */ + +#ifndef UPDATECOMMON_H +#define UPDATECOMMON_H + +#include "updatedefines.h" +#include <stdio.h> + +class UpdateLog +{ +public: + static UpdateLog & GetPrimaryLog() + { + static UpdateLog primaryLog; + return primaryLog; + } + + void Init(NS_tchar* sourcePath, const NS_tchar* fileName); + void Finish(); + void Flush(); + void Printf(const char *fmt, ... ); + void WarnPrintf(const char *fmt, ... ); + + ~UpdateLog() + { + Finish(); + } + +protected: + UpdateLog(); + FILE *logFP; + NS_tchar mTmpFilePath[MAXPATHLEN]; + NS_tchar mDstFilePath[MAXPATHLEN]; +}; + +bool IsValidFullPath(NS_tchar* fullPath); + +#define LOG_WARN(args) UpdateLog::GetPrimaryLog().WarnPrintf args +#define LOG(args) UpdateLog::GetPrimaryLog().Printf args +#define LogInit(PATHNAME_, FILENAME_) \ + UpdateLog::GetPrimaryLog().Init(PATHNAME_, FILENAME_) +#define LogFinish() UpdateLog::GetPrimaryLog().Finish() +#define LogFlush() UpdateLog::GetPrimaryLog().Flush() + +#endif diff --git a/toolkit/mozapps/update/common/updatedefines.h b/toolkit/mozapps/update/common/updatedefines.h new file mode 100644 index 000000000..760d2c4c4 --- /dev/null +++ b/toolkit/mozapps/update/common/updatedefines.h @@ -0,0 +1,142 @@ +/* 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/. */ + +#ifndef UPDATEDEFINES_H +#define UPDATEDEFINES_H + +#include "readstrings.h" + +#ifndef MAXPATHLEN +# ifdef PATH_MAX +# define MAXPATHLEN PATH_MAX +# elif defined(MAX_PATH) +# define MAXPATHLEN MAX_PATH +# elif defined(_MAX_PATH) +# define MAXPATHLEN _MAX_PATH +# elif defined(CCHMAXPATH) +# define MAXPATHLEN CCHMAXPATH +# else +# define MAXPATHLEN 1024 +# endif +#endif + +#if defined(XP_WIN) +# include <windows.h> +# include <shlwapi.h> +# include <direct.h> +# include <io.h> +# include <stdio.h> +# include <stdarg.h> + +# ifndef F_OK +# define F_OK 00 +# endif +# ifndef W_OK +# define W_OK 02 +# endif +# ifndef R_OK +# define R_OK 04 +# endif +# define S_ISDIR(s) (((s) & _S_IFMT) == _S_IFDIR) +# define S_ISREG(s) (((s) & _S_IFMT) == _S_IFREG) + +# define access _access + +# define putenv _putenv +# if defined(_MSC_VER) && _MSC_VER < 1900 +# define stat _stat +# endif +# define DELETE_DIR L"tobedeleted" +# define CALLBACK_BACKUP_EXT L".moz-callback" + +# define LOG_S "%S" +# define NS_T(str) L ## str +# define NS_SLASH NS_T('\\') + +static inline int mywcsprintf(WCHAR* dest, size_t count, const WCHAR* fmt, ...) +{ + size_t _count = count - 1; + va_list varargs; + va_start(varargs, fmt); + int result = _vsnwprintf(dest, count - 1, fmt, varargs); + va_end(varargs); + dest[_count] = L'\0'; + return result; +} +#define NS_tsnprintf mywcsprintf +# define NS_taccess _waccess +# define NS_tchdir _wchdir +# define NS_tchmod _wchmod +# define NS_tfopen _wfopen +# define NS_tmkdir(path, perms) _wmkdir(path) +# define NS_tremove _wremove +// _wrename is used to avoid the link tracking service. +# define NS_trename _wrename +# define NS_trmdir _wrmdir +# define NS_tstat _wstat +# define NS_tlstat _wstat // No symlinks on Windows +# define NS_tstat_t _stat +# define NS_tstrcat wcscat +# define NS_tstrcmp wcscmp +# define NS_tstricmp wcsicmp +# define NS_tstrcpy wcscpy +# define NS_tstrncpy wcsncpy +# define NS_tstrlen wcslen +# define NS_tstrchr wcschr +# define NS_tstrrchr wcsrchr +# define NS_tstrstr wcsstr +# include "win_dirent.h" +# define NS_tDIR DIR +# define NS_tdirent dirent +# define NS_topendir opendir +# define NS_tclosedir closedir +# define NS_treaddir readdir +#else +# include <sys/wait.h> +# include <unistd.h> + +#ifdef SOLARIS +# include <sys/stat.h> +#else +# include <fts.h> +#endif +# include <dirent.h> + +#ifdef XP_MACOSX +# include <sys/time.h> +#endif + +# define LOG_S "%s" +# define NS_T(str) str +# define NS_SLASH NS_T('/') +# define NS_tsnprintf snprintf +# define NS_taccess access +# define NS_tchdir chdir +# define NS_tchmod chmod +# define NS_tfopen fopen +# define NS_tmkdir mkdir +# define NS_tremove remove +# define NS_trename rename +# define NS_trmdir rmdir +# define NS_tstat stat +# define NS_tstat_t stat +# define NS_tlstat lstat +# define NS_tstrcat strcat +# define NS_tstrcmp strcmp +# define NS_tstricmp strcasecmp +# define NS_tstrcpy strcpy +# define NS_tstrncpy strncpy +# define NS_tstrlen strlen +# define NS_tstrrchr strrchr +# define NS_tstrstr strstr +# define NS_tDIR DIR +# define NS_tdirent dirent +# define NS_topendir opendir +# define NS_tclosedir closedir +# define NS_treaddir readdir +#endif + +#define BACKUP_EXT NS_T(".moz-backup") + +#endif diff --git a/toolkit/mozapps/update/common/updatehelper.cpp b/toolkit/mozapps/update/common/updatehelper.cpp new file mode 100644 index 000000000..afb89bacf --- /dev/null +++ b/toolkit/mozapps/update/common/updatehelper.cpp @@ -0,0 +1,609 @@ +/* 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 <windows.h> + +// Needed for CreateToolhelp32Snapshot +#include <tlhelp32.h> +#ifndef ONLY_SERVICE_LAUNCHING + +#include <stdio.h> +#include "shlobj.h" +#include "updatehelper.h" +#include "uachelper.h" +#include "pathhash.h" +#include "mozilla/UniquePtr.h" + +// Needed for PathAppendW +#include <shlwapi.h> + +using mozilla::MakeUnique; +using mozilla::UniquePtr; + +BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra); +BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, + LPCWSTR siblingFilePath, + LPCWSTR newFileName); + +/** + * Obtains the path of a file in the same directory as the specified file. + * + * @param destinationBuffer A buffer of size MAX_PATH + 1 to store the result. + * @param siblingFIlePath The path of another file in the same directory + * @param newFileName The filename of another file in the same directory + * @return TRUE if successful + */ +BOOL +PathGetSiblingFilePath(LPWSTR destinationBuffer, + LPCWSTR siblingFilePath, + LPCWSTR newFileName) +{ + if (wcslen(siblingFilePath) >= MAX_PATH) { + return FALSE; + } + + wcsncpy(destinationBuffer, siblingFilePath, MAX_PATH); + if (!PathRemoveFileSpecW(destinationBuffer)) { + return FALSE; + } + + if (wcslen(destinationBuffer) + wcslen(newFileName) >= MAX_PATH) { + return FALSE; + } + + return PathAppendSafe(destinationBuffer, newFileName); +} + +/** + * Starts the upgrade process for update of the service if it is + * already installed. + * + * @param installDir the installation directory where + * maintenanceservice_installer.exe is located. + * @return TRUE if successful + */ +BOOL +StartServiceUpdate(LPCWSTR installDir) +{ + // Get a handle to the local computer SCM database + SC_HANDLE manager = OpenSCManager(nullptr, nullptr, + SC_MANAGER_ALL_ACCESS); + if (!manager) { + return FALSE; + } + + // Open the service + SC_HANDLE svc = OpenServiceW(manager, SVC_NAME, + SERVICE_ALL_ACCESS); + if (!svc) { + CloseServiceHandle(manager); + return FALSE; + } + + // If we reach here, then the service is installed, so + // proceed with upgrading it. + + CloseServiceHandle(manager); + + // The service exists and we opened it, get the config bytes needed + DWORD bytesNeeded; + if (!QueryServiceConfigW(svc, nullptr, 0, &bytesNeeded) && + GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + CloseServiceHandle(svc); + return FALSE; + } + + // Get the service config information, in particular we want the binary + // path of the service. + UniquePtr<char[]> serviceConfigBuffer = MakeUnique<char[]>(bytesNeeded); + if (!QueryServiceConfigW(svc, + reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()), + bytesNeeded, &bytesNeeded)) { + CloseServiceHandle(svc); + return FALSE; + } + + CloseServiceHandle(svc); + + QUERY_SERVICE_CONFIGW &serviceConfig = + *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()); + + PathUnquoteSpacesW(serviceConfig.lpBinaryPathName); + + // Obtain the temp path of the maintenance service binary + WCHAR tmpService[MAX_PATH + 1] = { L'\0' }; + if (!PathGetSiblingFilePath(tmpService, serviceConfig.lpBinaryPathName, + L"maintenanceservice_tmp.exe")) { + return FALSE; + } + + // Get the new maintenance service path from the install dir + WCHAR newMaintServicePath[MAX_PATH + 1] = { L'\0' }; + wcsncpy(newMaintServicePath, installDir, MAX_PATH); + PathAppendSafe(newMaintServicePath, + L"maintenanceservice.exe"); + + // Copy the temp file in alongside the maintenace service. + // This is a requirement for maintenance service upgrades. + if (!CopyFileW(newMaintServicePath, tmpService, FALSE)) { + return FALSE; + } + + // Start the upgrade comparison process + STARTUPINFOW si = {0}; + si.cb = sizeof(STARTUPINFOW); + // No particular desktop because no UI + si.lpDesktop = L""; + PROCESS_INFORMATION pi = {0}; + WCHAR cmdLine[64] = { '\0' }; + wcsncpy(cmdLine, L"dummyparam.exe upgrade", + sizeof(cmdLine) / sizeof(cmdLine[0]) - 1); + BOOL svcUpdateProcessStarted = CreateProcessW(tmpService, + cmdLine, + nullptr, nullptr, FALSE, + 0, + nullptr, installDir, &si, &pi); + if (svcUpdateProcessStarted) { + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + return svcUpdateProcessStarted; +} + +#endif + +/** + * Executes a maintenance service command + * + * @param argc The total number of arguments in argv + * @param argv An array of null terminated strings to pass to the service, + * @return ERROR_SUCCESS if the service command was started. + * Less than 16000, a windows system error code from StartServiceW + * More than 20000, 20000 + the last state of the service constant if + * the last state is something other than stopped. + * 17001 if the SCM could not be opened + * 17002 if the service could not be opened +*/ +DWORD +StartServiceCommand(int argc, LPCWSTR* argv) +{ + DWORD lastState = WaitForServiceStop(SVC_NAME, 5); + if (lastState != SERVICE_STOPPED) { + return 20000 + lastState; + } + + // Get a handle to the SCM database. + SC_HANDLE serviceManager = OpenSCManager(nullptr, nullptr, + SC_MANAGER_CONNECT | + SC_MANAGER_ENUMERATE_SERVICE); + if (!serviceManager) { + return 17001; + } + + // Get a handle to the service. + SC_HANDLE service = OpenServiceW(serviceManager, + SVC_NAME, + SERVICE_START); + if (!service) { + CloseServiceHandle(serviceManager); + return 17002; + } + + // Wait at most 5 seconds trying to start the service in case of errors + // like ERROR_SERVICE_DATABASE_LOCKED or ERROR_SERVICE_REQUEST_TIMEOUT. + const DWORD maxWaitMS = 5000; + DWORD currentWaitMS = 0; + DWORD lastError = ERROR_SUCCESS; + while (currentWaitMS < maxWaitMS) { + BOOL result = StartServiceW(service, argc, argv); + if (result) { + lastError = ERROR_SUCCESS; + break; + } else { + lastError = GetLastError(); + } + Sleep(100); + currentWaitMS += 100; + } + CloseServiceHandle(service); + CloseServiceHandle(serviceManager); + return lastError; +} + +#ifndef ONLY_SERVICE_LAUNCHING + +/** + * Launch a service initiated action for a software update with the + * specified arguments. + * + * @param exePath The path of the executable to run + * @param argc The total number of arguments in argv + * @param argv An array of null terminated strings to pass to the exePath, + * argv[0] must be the path to the updater.exe + * @return ERROR_SUCCESS if successful + */ +DWORD +LaunchServiceSoftwareUpdateCommand(int argc, LPCWSTR* argv) +{ + // The service command is the same as the updater.exe command line except + // it has 2 extra args: 1) The Path to udpater.exe, and 2) the command + // being executed which is "software-update" + LPCWSTR *updaterServiceArgv = new LPCWSTR[argc + 2]; + updaterServiceArgv[0] = L"MozillaMaintenance"; + updaterServiceArgv[1] = L"software-update"; + + for (int i = 0; i < argc; ++i) { + updaterServiceArgv[i + 2] = argv[i]; + } + + // Execute the service command by starting the service with + // the passed in arguments. + DWORD ret = StartServiceCommand(argc + 2, updaterServiceArgv); + delete[] updaterServiceArgv; + return ret; +} + +/** + * Joins a base directory path with a filename. + * + * @param base The base directory path of size MAX_PATH + 1 + * @param extra The filename to append + * @return TRUE if the file name was successful appended to base + */ +BOOL +PathAppendSafe(LPWSTR base, LPCWSTR extra) +{ + if (wcslen(base) + wcslen(extra) >= MAX_PATH) { + return FALSE; + } + + return PathAppendW(base, extra); +} + +/** + * Sets update.status to a specific failure code + * + * @param updateDirPath The path of the update directory + * @return TRUE if successful + */ +BOOL +WriteStatusFailure(LPCWSTR updateDirPath, int errorCode) +{ + // The temp file is not removed on failure since there is client code that + // will remove it. + WCHAR tmpUpdateStatusFilePath[MAX_PATH + 1] = { L'\0' }; + if (GetTempFileNameW(updateDirPath, L"svc", 0, tmpUpdateStatusFilePath) == 0) { + return FALSE; + } + + HANDLE tmpStatusFile = CreateFileW(tmpUpdateStatusFilePath, GENERIC_WRITE, 0, + nullptr, CREATE_ALWAYS, 0, nullptr); + if (tmpStatusFile == INVALID_HANDLE_VALUE) { + return FALSE; + } + + char failure[32]; + sprintf(failure, "failed: %d", errorCode); + DWORD toWrite = strlen(failure); + DWORD wrote; + BOOL ok = WriteFile(tmpStatusFile, failure, + toWrite, &wrote, nullptr); + CloseHandle(tmpStatusFile); + + if (!ok || wrote != toWrite) { + return FALSE; + } + + WCHAR updateStatusFilePath[MAX_PATH + 1] = { L'\0' }; + wcsncpy(updateStatusFilePath, updateDirPath, MAX_PATH); + if (!PathAppendSafe(updateStatusFilePath, L"update.status")) { + return FALSE; + } + + if (MoveFileExW(tmpUpdateStatusFilePath, updateStatusFilePath, + MOVEFILE_REPLACE_EXISTING) == 0) { + return FALSE; + } + + return TRUE; +} + +#endif + +/** + * Waits for a service to enter a stopped state. + * This function does not stop the service, it just blocks until the service + * is stopped. + * + * @param serviceName The service to wait for. + * @param maxWaitSeconds The maximum number of seconds to wait + * @return state of the service after a timeout or when stopped. + * A value of 255 is returned for an error. Typical values are: + * SERVICE_STOPPED 0x00000001 + * SERVICE_START_PENDING 0x00000002 + * SERVICE_STOP_PENDING 0x00000003 + * SERVICE_RUNNING 0x00000004 + * SERVICE_CONTINUE_PENDING 0x00000005 + * SERVICE_PAUSE_PENDING 0x00000006 + * SERVICE_PAUSED 0x00000007 + * last status not set 0x000000CF + * Could no query status 0x000000DF + * Could not open service, access denied 0x000000EB + * Could not open service, invalid handle 0x000000EC + * Could not open service, invalid name 0x000000ED + * Could not open service, does not exist 0x000000EE + * Could not open service, other error 0x000000EF + * Could not open SCM, access denied 0x000000FD + * Could not open SCM, database does not exist 0x000000FE; + * Could not open SCM, other error 0x000000FF; + * Note: The strange choice of error codes above SERVICE_PAUSED are chosen + * in case Windows comes out with other service stats higher than 7, they + * would likely call it 8 and above. JS code that uses this in TestAUSHelper + * only handles values up to 255 so that's why we don't use GetLastError + * directly. + */ +DWORD +WaitForServiceStop(LPCWSTR serviceName, DWORD maxWaitSeconds) +{ + // 0x000000CF is defined above to be not set + DWORD lastServiceState = 0x000000CF; + + // Get a handle to the SCM database. + SC_HANDLE serviceManager = OpenSCManager(nullptr, nullptr, + SC_MANAGER_CONNECT | + SC_MANAGER_ENUMERATE_SERVICE); + if (!serviceManager) { + DWORD lastError = GetLastError(); + switch(lastError) { + case ERROR_ACCESS_DENIED: + return 0x000000FD; + case ERROR_DATABASE_DOES_NOT_EXIST: + return 0x000000FE; + default: + return 0x000000FF; + } + } + + // Get a handle to the service. + SC_HANDLE service = OpenServiceW(serviceManager, + serviceName, + SERVICE_QUERY_STATUS); + if (!service) { + DWORD lastError = GetLastError(); + CloseServiceHandle(serviceManager); + switch(lastError) { + case ERROR_ACCESS_DENIED: + return 0x000000EB; + case ERROR_INVALID_HANDLE: + return 0x000000EC; + case ERROR_INVALID_NAME: + return 0x000000ED; + case ERROR_SERVICE_DOES_NOT_EXIST: + return 0x000000EE; + default: + return 0x000000EF; + } + } + + DWORD currentWaitMS = 0; + SERVICE_STATUS_PROCESS ssp; + ssp.dwCurrentState = lastServiceState; + while (currentWaitMS < maxWaitSeconds * 1000) { + DWORD bytesNeeded; + if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, (LPBYTE)&ssp, + sizeof(SERVICE_STATUS_PROCESS), &bytesNeeded)) { + DWORD lastError = GetLastError(); + switch (lastError) { + case ERROR_INVALID_HANDLE: + ssp.dwCurrentState = 0x000000D9; + break; + case ERROR_ACCESS_DENIED: + ssp.dwCurrentState = 0x000000DA; + break; + case ERROR_INSUFFICIENT_BUFFER: + ssp.dwCurrentState = 0x000000DB; + break; + case ERROR_INVALID_PARAMETER: + ssp.dwCurrentState = 0x000000DC; + break; + case ERROR_INVALID_LEVEL: + ssp.dwCurrentState = 0x000000DD; + break; + case ERROR_SHUTDOWN_IN_PROGRESS: + ssp.dwCurrentState = 0x000000DE; + break; + // These 3 errors can occur when the service is not yet stopped but + // it is stopping. + case ERROR_INVALID_SERVICE_CONTROL: + case ERROR_SERVICE_CANNOT_ACCEPT_CTRL: + case ERROR_SERVICE_NOT_ACTIVE: + currentWaitMS += 50; + Sleep(50); + continue; + default: + ssp.dwCurrentState = 0x000000DF; + } + + // We couldn't query the status so just break out + break; + } + + // The service is already in use. + if (ssp.dwCurrentState == SERVICE_STOPPED) { + break; + } + currentWaitMS += 50; + Sleep(50); + } + + lastServiceState = ssp.dwCurrentState; + CloseServiceHandle(service); + CloseServiceHandle(serviceManager); + return lastServiceState; +} + +#ifndef ONLY_SERVICE_LAUNCHING + +/** + * Determines if there is at least one process running for the specified + * application. A match will be found across any session for any user. + * + * @param process The process to check for existance + * @return ERROR_NOT_FOUND if the process was not found + * ERROR_SUCCESS if the process was found and there were no errors + * Other Win32 system error code for other errors +**/ +DWORD +IsProcessRunning(LPCWSTR filename) +{ + // Take a snapshot of all processes in the system. + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (INVALID_HANDLE_VALUE == snapshot) { + return GetLastError(); + } + + PROCESSENTRY32W processEntry; + processEntry.dwSize = sizeof(PROCESSENTRY32W); + if (!Process32FirstW(snapshot, &processEntry)) { + DWORD lastError = GetLastError(); + CloseHandle(snapshot); + return lastError; + } + + do { + if (wcsicmp(filename, processEntry.szExeFile) == 0) { + CloseHandle(snapshot); + return ERROR_SUCCESS; + } + } while (Process32NextW(snapshot, &processEntry)); + CloseHandle(snapshot); + return ERROR_NOT_FOUND; +} + +/** + * Waits for the specified applicaiton to exit. + * + * @param filename The application to wait for. + * @param maxSeconds The maximum amount of seconds to wait for all + * instances of the application to exit. + * @return ERROR_SUCCESS if no instances of the application exist + * WAIT_TIMEOUT if the process is still running after maxSeconds. + * Any other Win32 system error code. +*/ +DWORD +WaitForProcessExit(LPCWSTR filename, DWORD maxSeconds) +{ + DWORD applicationRunningError = WAIT_TIMEOUT; + for(DWORD i = 0; i < maxSeconds; i++) { + DWORD applicationRunningError = IsProcessRunning(filename); + if (ERROR_NOT_FOUND == applicationRunningError) { + return ERROR_SUCCESS; + } + Sleep(1000); + } + + if (ERROR_SUCCESS == applicationRunningError) { + return WAIT_TIMEOUT; + } + + return applicationRunningError; +} + +/** + * Determines if the fallback key exists or not + * + * @return TRUE if the fallback key exists and there was no error checking +*/ +BOOL +DoesFallbackKeyExist() +{ + HKEY testOnlyFallbackKey; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, + TEST_ONLY_FALLBACK_KEY_PATH, 0, + KEY_READ | KEY_WOW64_64KEY, + &testOnlyFallbackKey) != ERROR_SUCCESS) { + return FALSE; + } + + RegCloseKey(testOnlyFallbackKey); + return TRUE; +} + +#endif + +/** + * Determines if the file system for the specified file handle is local + * @param file path to check the filesystem type for, must be at most MAX_PATH + * @param isLocal out parameter which will hold TRUE if the drive is local + * @return TRUE if the call succeeded +*/ +BOOL +IsLocalFile(LPCWSTR file, BOOL &isLocal) +{ + WCHAR rootPath[MAX_PATH + 1] = { L'\0' }; + if (wcslen(file) > MAX_PATH) { + return FALSE; + } + + wcsncpy(rootPath, file, MAX_PATH); + PathStripToRootW(rootPath); + isLocal = GetDriveTypeW(rootPath) == DRIVE_FIXED; + return TRUE; +} + + +/** + * Determines the DWORD value of a registry key value + * + * @param key The base key to where the value name exists + * @param valueName The name of the value + * @param retValue Out parameter which will hold the value + * @return TRUE on success +*/ +static BOOL +GetDWORDValue(HKEY key, LPCWSTR valueName, DWORD &retValue) +{ + DWORD regDWORDValueSize = sizeof(DWORD); + LONG retCode = RegQueryValueExW(key, valueName, 0, nullptr, + reinterpret_cast<LPBYTE>(&retValue), + ®DWORDValueSize); + return ERROR_SUCCESS == retCode; +} + +/** + * Determines if the the system's elevation type allows + * unprmopted elevation. + * + * @param isUnpromptedElevation Out parameter which specifies if unprompted + * elevation is allowed. + * @return TRUE if the user can actually elevate and the value was obtained + * successfully. +*/ +BOOL +IsUnpromptedElevation(BOOL &isUnpromptedElevation) +{ + if (!UACHelper::CanUserElevate()) { + return FALSE; + } + + LPCWSTR UACBaseRegKey = + L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"; + HKEY baseKey; + LONG retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE, + UACBaseRegKey, 0, + KEY_READ, &baseKey); + if (retCode != ERROR_SUCCESS) { + return FALSE; + } + + DWORD consent, secureDesktop; + BOOL success = GetDWORDValue(baseKey, L"ConsentPromptBehaviorAdmin", + consent); + success = success && + GetDWORDValue(baseKey, L"PromptOnSecureDesktop", secureDesktop); + isUnpromptedElevation = !consent && !secureDesktop; + + RegCloseKey(baseKey); + return success; +} diff --git a/toolkit/mozapps/update/common/updatehelper.h b/toolkit/mozapps/update/common/updatehelper.h new file mode 100644 index 000000000..3f010df49 --- /dev/null +++ b/toolkit/mozapps/update/common/updatehelper.h @@ -0,0 +1,29 @@ +/* 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/. */ + +BOOL StartServiceUpdate(LPCWSTR installDir); +DWORD LaunchServiceSoftwareUpdateCommand(int argc, LPCWSTR *argv); +BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode); +DWORD WaitForServiceStop(LPCWSTR serviceName, DWORD maxWaitSeconds); +DWORD WaitForProcessExit(LPCWSTR filename, DWORD maxSeconds); +DWORD IsProcessRunning(LPCWSTR filename); +BOOL DoesFallbackKeyExist(); +BOOL IsLocalFile(LPCWSTR file, BOOL &isLocal); +DWORD StartServiceCommand(int argc, LPCWSTR* argv); +BOOL IsUnpromptedElevation(BOOL &isUnpromptedElevation); + +#define SVC_NAME L"MozillaMaintenance" + +#define BASE_SERVICE_REG_KEY \ + L"SOFTWARE\\Mozilla\\MaintenanceService" + +// The test only fallback key, as its name implies, is only present on machines +// that will use automated tests. Since automated tests always run from a +// different directory for each test, the presence of this key bypasses the +// "This is a valid installation directory" check. This key also stores +// the allowed name and issuer for cert checks so that the cert check +// code can still be run unchanged. +#define TEST_ONLY_FALLBACK_KEY_PATH \ + BASE_SERVICE_REG_KEY L"\\3932ecacee736d366d6436db0f55bce4" + diff --git a/toolkit/mozapps/update/common/win_dirent.h b/toolkit/mozapps/update/common/win_dirent.h new file mode 100644 index 000000000..28f5317ff --- /dev/null +++ b/toolkit/mozapps/update/common/win_dirent.h @@ -0,0 +1,32 @@ +/* -*- 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/. */ + +#ifndef WINDIRENT_H__ +#define WINDIRENT_H__ + +#ifndef XP_WIN +#error This library should only be used on Windows +#endif + +#include <windows.h> + +struct DIR { + explicit DIR(const WCHAR* path); + ~DIR(); + HANDLE findHandle; + WCHAR name[MAX_PATH]; +}; + +struct dirent { + dirent(); + WCHAR d_name[MAX_PATH]; +}; + +DIR* opendir(const WCHAR* path); +int closedir(DIR* dir); +dirent* readdir(DIR* dir); + +#endif // WINDIRENT_H__ diff --git a/toolkit/mozapps/update/content/history.js b/toolkit/mozapps/update/content/history.js new file mode 100644 index 000000000..32a098de5 --- /dev/null +++ b/toolkit/mozapps/update/content/history.js @@ -0,0 +1,70 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +var gUpdateHistory = { + _view: null, + + /** + * Initialize the User Interface + */ + onLoad: function() { + this._view = document.getElementById("historyItems"); + + var um = + Components.classes["@mozilla.org/updates/update-manager;1"]. + getService(Components.interfaces.nsIUpdateManager); + var uc = um.updateCount; + if (uc) { + while (this._view.hasChildNodes()) + this._view.removeChild(this._view.firstChild); + + var bundle = document.getElementById("updateBundle"); + + for (var i = 0; i < uc; ++i) { + var update = um.getUpdateAt(i); + if (!update || !update.name) + continue; + + // Don't display updates that are downloading since they don't have + // valid statusText for the UI (bug 485493). + if (!update.statusText) + continue; + + var element = document.createElementNS(NS_XUL, "update"); + this._view.appendChild(element); + element.name = bundle.getFormattedString("updateFullName", + [update.name, update.buildID]); + element.type = bundle.getString("updateType_" + update.type); + element.installDate = this._formatDate(update.installDate); + if (update.detailsURL) + element.detailsURL = update.detailsURL; + else + element.hideDetailsURL = true; + element.status = update.statusText; + } + } + var cancelbutton = document.documentElement.getButton("cancel"); + cancelbutton.focus(); + }, + + /** + * Formats a date into human readable form + * @param seconds + * A date in seconds since 1970 epoch + * @returns A human readable date string + */ + _formatDate: function(seconds) { + var date = new Date(seconds); + const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"] + .getService(Components.interfaces.nsIXULChromeRegistry) + .getSelectedLocale("global", true); + const dtOptions = { year: 'numeric', month: 'long', day: 'numeric', + hour: 'numeric', minute: 'numeric', second: 'numeric' }; + return date.toLocaleString(locale, dtOptions); + } +}; + diff --git a/toolkit/mozapps/update/content/history.xul b/toolkit/mozapps/update/content/history.xul new file mode 100644 index 000000000..92a32b6ef --- /dev/null +++ b/toolkit/mozapps/update/content/history.xul @@ -0,0 +1,39 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE dialog [ +<!ENTITY % historyDTD SYSTEM "chrome://mozapps/locale/update/history.dtd"> +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%historyDTD; +%brandDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://mozapps/content/update/updates.css"?> +<?xml-stylesheet href="chrome://mozapps/skin/update/updates.css"?> + +<dialog id="history" windowtype="Update:History" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + style="width: 35em;" + buttons="cancel" + defaultButton="cancel" + buttonlabelcancel="&closebutton.label;" + title="&history.title;" + onload="gUpdateHistory.onLoad();"> + + <script type="application/javascript" + src="chrome://mozapps/content/update/history.js"/> + + <stringbundle id="updateBundle" + src="chrome://mozapps/locale/update/updates.properties"/> + + <label>&history.intro;</label> + <separator class="thin"/> + <richlistbox id="historyItems" flex="1"> + <label>&noupdates.label;</label> + </richlistbox> + <separator class="thin"/> +</dialog> diff --git a/toolkit/mozapps/update/content/updates.css b/toolkit/mozapps/update/content/updates.css new file mode 100644 index 000000000..e83c3be03 --- /dev/null +++ b/toolkit/mozapps/update/content/updates.css @@ -0,0 +1,33 @@ +/* Stop animations on pages that aren't on the current page (bug 341749). */ +#updates:not([currentpageid="checking"]) #checkingProgress, +#updates:not([currentpageid="downloading"]) #downloadProgress { + display: none; +} + +/* Hide the wizard's header so the size of the billboard can size the window + on creation. A custom header will be used in its place when a header is + needed. */ +.wizard-header { + display: none; +} + +/* Display the custom header */ +.update-header { + display: -moz-box !important; +} + +/* Custom header implementation based on the Wizard's header. This allows the + size of the billboard's remotecontent to size the window since it does not + have an updateheader on the billboard page. */ +updateheader { + -moz-binding: url("chrome://mozapps/content/update/updates.xml#updateheader"); + display: -moz-box; + -moz-box-orient: horizontal; +} + +/* Update History Window */ +update { + -moz-binding: url("chrome://mozapps/content/update/updates.xml#update"); + display: -moz-box; + -moz-box-orient: vertical; +} diff --git a/toolkit/mozapps/update/content/updates.js b/toolkit/mozapps/update/content/updates.js new file mode 100644 index 000000000..6e8de7275 --- /dev/null +++ b/toolkit/mozapps/update/content/updates.js @@ -0,0 +1,1399 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +'use strict'; + +/* import-globals-from ../../../content/contentAreaUtils.js */ + +// Firefox's macBrowserOverlay.xul includes scripts that define Cc, Ci, and Cr +// so we have to use different names. +const {classes: CoC, interfaces: CoI, results: CoR, utils: CoU} = Components; + +/* globals DownloadUtils, Services, AUSTLMY */ +CoU.import("resource://gre/modules/DownloadUtils.jsm", this); +CoU.import("resource://gre/modules/Services.jsm", this); +CoU.import("resource://gre/modules/UpdateTelemetry.jsm", this); + +const XMLNS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors"; +const PREF_APP_UPDATE_CERT_ERRORS = "app.update.cert.errors"; +const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never"; +const PREF_APP_UPDATE_ENABLED = "app.update.enabled"; +const PREF_APP_UPDATE_LOG = "app.update.log"; +const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported"; +const PREF_APP_UPDATE_TEST_LOOP = "app.update.test.loop"; +const PREF_APP_UPDATE_URL_MANUAL = "app.update.url.manual"; + +const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never."; + +const UPDATE_TEST_LOOP_INTERVAL = 2000; + +const URI_UPDATES_PROPERTIES = "chrome://mozapps/locale/update/updates.properties"; + +const STATE_DOWNLOADING = "downloading"; +const STATE_PENDING = "pending"; +const STATE_PENDING_SERVICE = "pending-service"; +const STATE_PENDING_ELEVATE = "pending-elevate"; +const STATE_APPLYING = "applying"; +const STATE_APPLIED = "applied"; +const STATE_APPLIED_SERVICE = "applied-service"; +const STATE_SUCCEEDED = "succeeded"; +const STATE_DOWNLOAD_FAILED = "download-failed"; +const STATE_FAILED = "failed"; + +const SRCEVT_FOREGROUND = 1; +const SRCEVT_BACKGROUND = 2; + +const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110; + +var gLogEnabled = false; +var gUpdatesFoundPageId; + +// Notes: +// 1. use the wizard's goTo method whenever possible to change the wizard +// page since it is simpler than most other methods and behaves nicely with +// mochitests. +// 2. using a page's onPageShow method to then change to a different page will +// of course call that page's onPageShow method which can make mochitests +// overly complicated and fragile so avoid doing this if at all possible. +// This is why a page's next attribute is set prior to the page being shown +// whenever possible. + +/** + * Logs a string to the error console. + * @param string + * The string to write to the error console.. + */ +function LOG(module, string) { + if (gLogEnabled) { + dump("*** AUS:UI " + module + ":" + string + "\n"); + Services.console.logStringMessage("AUS:UI " + module + ":" + string); + } +} + +/** + * Opens a URL using the event target's url attribute for the URL. This is a + * workaround for Bug 263433 which prevents respecting tab browser preferences + * for where to open a URL. + */ +function openUpdateURL(event) { + if (event.button == 0) + openURL(event.target.getAttribute("url")); +} + +/** + * Gets a preference value, handling the case where there is no default. + * @param func + * The name of the preference function to call, on nsIPrefBranch + * @param preference + * The name of the preference + * @param defaultValue + * The default value to return in the event the preference has + * no setting + * @returns The value of the preference, or undefined if there was no + * user or default value. + */ +function getPref(func, preference, defaultValue) { + try { + return Services.prefs[func](preference); + } + catch (e) { + LOG("General", "getPref - failed to get preference: " + preference); + } + return defaultValue; +} + +/** + * A set of shared data and control functions for the wizard as a whole. + */ +var gUpdates = { + /** + * The nsIUpdate object being used by this window (either for downloading, + * notification or both). + */ + update: null, + + /** + * The updates.properties <stringbundle> element. + */ + strings: null, + + /** + * The Application brandShortName (e.g. "Firefox") + */ + brandName: null, + + /** + * The <wizard> element + */ + wiz: null, + + /** + * Whether to run the unload handler. This will be set to false when the user + * exits the wizard via onWizardCancel or onWizardFinish. + */ + _runUnload: true, + + /** + * Submit on close telemtry values for the update wizard. + * @param pageID + * The page id for the last page displayed. + */ + _submitTelemetry: function(aPageID) { + AUSTLMY.pingWizLastPageCode(aPageID); + }, + + /** + * Helper function for setButtons + * Resets button to original label & accesskey if string is null. + */ + _setButton: function(button, string) { + if (string) { + var label = this.getAUSString(string); + if (label.indexOf("%S") != -1) + label = label.replace(/%S/, this.brandName); + button.label = label; + button.setAttribute("accesskey", + this.getAUSString(string + ".accesskey")); + } else { + button.label = button.defaultLabel; + button.setAttribute("accesskey", button.defaultAccesskey); + } + }, + + /** + * Sets the attributes needed for this Wizard's control buttons (labels, + * disabled, hidden, etc.) + * @param extra1ButtonString + * The property in the stringbundle containing the label to put on + * the first extra button, or null to hide the first extra button. + * @param extra2ButtonString + * The property in the stringbundle containing the label to put on + * the second extra button, or null to hide the second extra button. + * @param nextFinishButtonString + * The property in the stringbundle containing the label to put on + * the Next / Finish button, or null to hide the button. The Next and + * Finish buttons are never displayed at the same time in a wizard + * with the the Finish button only being displayed when there are no + * additional pages to display in the wizard. + * @param canAdvance + * true if the wizard can be advanced (e.g. the next / finish button + * should be enabled), false otherwise. + * @param showCancel + * true if the wizard's cancel button should be shown, false + * otherwise. If not specified this will default to false. + * + * Note: + * Per Bug 324121 the wizard should not look like a wizard and to accomplish + * this the back button is never displayed. This causes the wizard buttons to + * be arranged as follows on Windows with the next and finish buttons never + * being displayed at the same time. + * +--------------------------------------------------------------+ + * | [ extra1 ] [ extra2 ] [ next or finish ] | + * +--------------------------------------------------------------+ + */ + setButtons: function(extra1ButtonString, extra2ButtonString, + nextFinishButtonString, canAdvance, showCancel) { + this.wiz.canAdvance = canAdvance; + + var bnf = this.wiz.getButton(this.wiz.onLastPage ? "finish" : "next"); + var be1 = this.wiz.getButton("extra1"); + var be2 = this.wiz.getButton("extra2"); + var bc = this.wiz.getButton("cancel"); + + // Set the labels for the next / finish, extra1, and extra2 buttons + this._setButton(bnf, nextFinishButtonString); + this._setButton(be1, extra1ButtonString); + this._setButton(be2, extra2ButtonString); + + bnf.hidden = bnf.disabled = !nextFinishButtonString; + be1.hidden = be1.disabled = !extra1ButtonString; + be2.hidden = be2.disabled = !extra2ButtonString; + bc.hidden = bc.disabled = !showCancel; + + // Hide and disable the back button each time setButtons is called + // (see bug 464765). + var btn = this.wiz.getButton("back"); + btn.hidden = btn.disabled = true; + + // Hide and disable the finish button if not on the last page or the next + // button if on the last page each time setButtons is called. + btn = this.wiz.getButton(this.wiz.onLastPage ? "next" : "finish"); + btn.hidden = btn.disabled = true; + }, + + getAUSString: function(key, strings) { + if (strings) + return this.strings.getFormattedString(key, strings); + return this.strings.getString(key); + }, + + never: function () { + // If the user clicks "No Thanks", we should not prompt them to update to + // this version again unless they manually select "Check for Updates..." + // which will clear all of the "never" prefs. There are currently two + // "never" prefs: the older PREFBRANCH_APP_UPDATE_NEVER as well as the + // OSX-only PREF_APP_UPDATE_ELEVATE_NEVER. We set both of these prefs (if + // applicable) to ensure that we don't prompt the user regardless of which + // pref is checked. + let neverPrefName = PREFBRANCH_APP_UPDATE_NEVER + this.update.appVersion; + Services.prefs.setBoolPref(neverPrefName, true); + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (aus.elevationRequired) { + Services.prefs.setCharPref(PREF_APP_UPDATE_ELEVATE_NEVER, + this.update.appVersion); + } + }, + + /** + * A hash of |pageid| attribute to page object. Can be used to dispatch + * function calls to the appropriate page. + */ + _pages: { }, + + /** + * Called when the user presses the "Finish" button on the wizard, dispatches + * the function call to the selected page. + */ + onWizardFinish: function() { + this._runUnload = false; + var pageid = document.documentElement.currentPage.pageid; + if ("onWizardFinish" in this._pages[pageid]) + this._pages[pageid].onWizardFinish(); + this._submitTelemetry(pageid); + }, + + /** + * Called when the user presses the "Cancel" button on the wizard, dispatches + * the function call to the selected page. + */ + onWizardCancel: function() { + this._runUnload = false; + var pageid = document.documentElement.currentPage.pageid; + if ("onWizardCancel" in this._pages[pageid]) + this._pages[pageid].onWizardCancel(); + this._submitTelemetry(pageid); + }, + + /** + * Called when the user presses the "Next" button on the wizard, dispatches + * the function call to the selected page. + */ + onWizardNext: function() { + var cp = document.documentElement.currentPage; + if (!cp) + return; + var pageid = cp.pageid; + if ("onWizardNext" in this._pages[pageid]) + this._pages[pageid].onWizardNext(); + }, + + /** + * The checking process that spawned this update UI. There are two types: + * SRCEVT_FOREGROUND: + * Some user-generated event caused this UI to appear, e.g. the Help + * menu item or the button in preferences. When in this mode, the UI + * should remain active for the duration of the download. + * SRCEVT_BACKGROUND: + * A background update check caused this UI to appear, probably because + * the user has the app.update.auto preference set to false. + */ + sourceEvent: SRCEVT_FOREGROUND, + + /** + * Helper function for onLoad + * Saves default button label & accesskey for use by _setButton + */ + _cacheButtonStrings: function (buttonName) { + var button = this.wiz.getButton(buttonName); + button.defaultLabel = button.label; + button.defaultAccesskey = button.getAttribute("accesskey"); + }, + + /** + * Called when the wizard UI is loaded. + */ + onLoad: function() { + this.wiz = document.documentElement; + + gLogEnabled = getPref("getBoolPref", PREF_APP_UPDATE_LOG, false); + + this.strings = document.getElementById("updateStrings"); + var brandStrings = document.getElementById("brandStrings"); + this.brandName = brandStrings.getString("brandShortName"); + + var pages = this.wiz.childNodes; + for (var i = 0; i < pages.length; ++i) { + var page = pages[i]; + if (page.localName == "wizardpage") + this._pages[page.pageid] = eval(page.getAttribute("object")); + } + + // Cache the standard button labels in case we need to restore them + this._cacheButtonStrings("next"); + this._cacheButtonStrings("finish"); + this._cacheButtonStrings("extra1"); + this._cacheButtonStrings("extra2"); + + // Advance to the Start page. + this.getStartPageID(function(startPageID) { + LOG("gUpdates", "onLoad - setting current page to startpage " + startPageID); + gUpdates.wiz.currentPage = document.getElementById(startPageID); + }); + }, + + /** + * Called when the wizard UI is unloaded. + */ + onUnload: function() { + if (this._runUnload) { + var cp = this.wiz.currentPage; + if (cp.pageid != "finished" && cp.pageid != "finishedBackground") + this.onWizardCancel(); + } + }, + + /** + * Gets the ID of the <wizardpage> object that should be displayed first. This + * is an asynchronous method that passes the resulting object to a callback + * function. + * + * This is determined by how we were called by the update prompt: + * + * Prompt Method: Arg0: Update State: Src Event: Failed: Result: + * showUpdateAvailable nsIUpdate obj -- background -- updatesfoundbasic + * showUpdateDownloaded nsIUpdate obj pending background -- finishedBackground + * showUpdateError nsIUpdate obj failed either partial errorpatching + * showUpdateError nsIUpdate obj failed either complete errors + * checkForUpdates null -- foreground -- checking + * checkForUpdates null downloading foreground -- downloading + * + * @param aCallback + * A callback to pass the <wizardpage> object to be displayed first to. + */ + getStartPageID: function(aCallback) { + if ("arguments" in window && window.arguments[0]) { + var arg0 = window.arguments[0]; + if (arg0 instanceof CoI.nsIUpdate) { + // If the first argument is a nsIUpdate object, we are notifying the + // user that the background checking found an update that requires + // their permission to install, and it's ready for download. + this.setUpdate(arg0); + if (this.update.errorCode == BACKGROUNDCHECK_MULTIPLE_FAILURES) { + aCallback("errorextra"); + return; + } + + if (this.update.unsupported) { + aCallback("unsupported"); + return; + } + + var p = this.update.selectedPatch; + if (p) { + let state = p.state; + let patchFailed = this.update.getProperty("patchingFailed"); + if (patchFailed) { + if (patchFailed != "partial" || this.update.patchCount != 2) { + // If the complete patch failed, which is far less likely, show + // the error text held by the update object in the generic errors + // page, triggered by the |STATE_DOWNLOAD_FAILED| state. This also + // handles the case when an elevation was cancelled on Mac OS X. + state = STATE_DOWNLOAD_FAILED; + } else { + // If the system failed to apply the partial patch, show the + // screen which best describes this condition, which is triggered + // by the |STATE_FAILED| state. + state = STATE_FAILED; + } + } + + // Now select the best page to start with, given the current state of + // the Update. + switch (state) { + case STATE_PENDING: + case STATE_PENDING_SERVICE: + case STATE_PENDING_ELEVATE: + case STATE_APPLIED: + case STATE_APPLIED_SERVICE: + this.sourceEvent = SRCEVT_BACKGROUND; + aCallback("finishedBackground"); + return; + case STATE_DOWNLOADING: + aCallback("downloading"); + return; + case STATE_FAILED: + window.getAttention(); + aCallback("errorpatching"); + return; + case STATE_DOWNLOAD_FAILED: + case STATE_APPLYING: + aCallback("errors"); + return; + } + } + + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (!aus.canApplyUpdates) { + aCallback("manualUpdate"); + return; + } + + aCallback(this.updatesFoundPageId); + return; + } + } + else { + var um = CoC["@mozilla.org/updates/update-manager;1"]. + getService(CoI.nsIUpdateManager); + if (um.activeUpdate) { + this.setUpdate(um.activeUpdate); + aCallback("downloading"); + return; + } + } + aCallback("checking"); + }, + + /** + * Returns the string page ID for the appropriate updates found page based + * on the update's metadata. + */ + get updatesFoundPageId() { + if (gUpdatesFoundPageId) + return gUpdatesFoundPageId; + return gUpdatesFoundPageId = "updatesfoundbasic"; + }, + + /** + * Sets the Update object for this wizard + * @param update + * The update object + */ + setUpdate: function(update) { + this.update = update; + if (this.update) + this.update.QueryInterface(CoI.nsIWritablePropertyBag); + } +}; + +/** + * The "Checking for Updates" page. Provides feedback on the update checking + * process. + */ +var gCheckingPage = { + /** + * The nsIUpdateChecker that is currently checking for updates. We hold onto + * this so we can cancel the update check if the user closes the window. + */ + _checker: null, + + /** + * Initialize + */ + onPageShow: function() { + gUpdates.setButtons(null, null, null, false, true); + gUpdates.wiz.getButton("cancel").focus(); + + // Clear all of the "never" prefs to handle the scenario where the user + // clicked "never" for an update, selected "Check for Updates...", and + // then canceled. If we don't clear the "never" prefs future + // notifications will never happen. + Services.prefs.deleteBranch(PREFBRANCH_APP_UPDATE_NEVER); + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); + } + + // The user will be notified if there is an error so clear the background + // check error count. + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS); + } + + // The preference will be set back to true if the system is still + // unsupported. + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED); + } + + this._checker = CoC["@mozilla.org/updates/update-checker;1"]. + createInstance(CoI.nsIUpdateChecker); + this._checker.checkForUpdates(this.updateListener, true); + }, + + /** + * The user has closed the window, either by pressing cancel or using a Window + * Manager control, so stop checking for updates. + */ + onWizardCancel: function() { + this._checker.stopChecking(CoI.nsIUpdateChecker.CURRENT_CHECK); + }, + + /** + * An object implementing nsIUpdateCheckListener that is notified as the + * update check commences. + */ + updateListener: { + /** + * See nsIUpdateCheckListener + */ + onCheckComplete: function(request, updates, updateCount) { + var aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + gUpdates.setUpdate(aus.selectUpdate(updates, updates.length)); + if (gUpdates.update) { + LOG("gCheckingPage", "onCheckComplete - update found"); + if (gUpdates.update.unsupported) { + gUpdates.wiz.goTo("unsupported"); + return; + } + + if (!aus.canApplyUpdates || gUpdates.update.elevationFailure) { + // Prevent multiple notifications for the same update when the user is + // unable to apply updates. + gUpdates.never(); + gUpdates.wiz.goTo("manualUpdate"); + return; + } + + gUpdates.wiz.goTo(gUpdates.updatesFoundPageId); + return; + } + + LOG("gCheckingPage", "onCheckComplete - no update found"); + gUpdates.wiz.goTo("noupdatesfound"); + }, + + /** + * See nsIUpdateCheckListener + */ + onError: function(request, update) { + LOG("gCheckingPage", "onError - proceeding to error page"); + gUpdates.setUpdate(update); + gUpdates.wiz.goTo("errors"); + }, + + /** + * See nsISupports.idl + */ + QueryInterface: function(aIID) { + if (!aIID.equals(CoI.nsIUpdateCheckListener) && + !aIID.equals(CoI.nsISupports)) + throw CoR.NS_ERROR_NO_INTERFACE; + return this; + } + } +}; + +/** + * The "No Updates Are Available" page + */ +var gNoUpdatesPage = { + /** + * Initialize + */ + onPageShow: function() { + LOG("gNoUpdatesPage", "onPageShow - could not select an appropriate " + + "update. Either there were no updates or |selectUpdate| failed"); + + if (getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true)) + document.getElementById("noUpdatesAutoEnabled").hidden = false; + else + document.getElementById("noUpdatesAutoDisabled").hidden = false; + + gUpdates.setButtons(null, null, "okButton", true); + gUpdates.wiz.getButton("finish").focus(); + } +}; + +/** + * The "Unable to Update" page. Provides the user information about why they + * were unable to update and a manual download url. + */ +var gManualUpdatePage = { + onPageShow: function() { + var manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_MANUAL); + var manualUpdateLinkLabel = document.getElementById("manualUpdateLinkLabel"); + manualUpdateLinkLabel.value = manualURL; + manualUpdateLinkLabel.setAttribute("url", manualURL); + + gUpdates.setButtons(null, null, "okButton", true); + gUpdates.wiz.getButton("finish").focus(); + } +}; + +/** + * The "System Unsupported" page. Provides the user with information about their + * system no longer being supported and an url for more information. + */ +var gUnsupportedPage = { + onPageShow: function() { + Services.prefs.setBoolPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, true); + if (gUpdates.update.detailsURL) { + let unsupportedLinkLabel = document.getElementById("unsupportedLinkLabel"); + unsupportedLinkLabel.setAttribute("url", gUpdates.update.detailsURL); + } + + gUpdates.setButtons(null, null, "okButton", true); + gUpdates.wiz.getButton("finish").focus(); + } +}; + +/** + * The "Updates Are Available" page. Provides the user information about the + * available update. + */ +var gUpdatesFoundBasicPage = { + /** + * Initialize + */ + onPageShow: function() { + gUpdates.wiz.canRewind = false; + var update = gUpdates.update; + gUpdates.setButtons("askLaterButton", + update.showNeverForVersion ? "noThanksButton" : null, + "updateButton_" + update.type, true); + var btn = gUpdates.wiz.getButton("next"); + btn.focus(); + + var updateName = update.name; + if (update.channel == "nightly") { + updateName = gUpdates.getAUSString("updateNightlyName", + [gUpdates.brandName, + update.displayVersion, + update.buildID]); + } + var updateNameElement = document.getElementById("updateName"); + updateNameElement.value = updateName; + + var introText = gUpdates.getAUSString("intro_" + update.type, + [gUpdates.brandName, update.displayVersion]); + var introElem = document.getElementById("updatesFoundInto"); + introElem.setAttribute("severity", update.type); + introElem.textContent = introText; + + var updateMoreInfoURL = document.getElementById("updateMoreInfoURL"); + if (update.detailsURL) + updateMoreInfoURL.setAttribute("url", update.detailsURL); + else + updateMoreInfoURL.hidden = true; + + var updateTitle = gUpdates.getAUSString("updatesfound_" + update.type + + ".title"); + document.getElementById("updatesFoundBasicHeader").setAttribute("label", updateTitle); + }, + + onExtra1: function() { + gUpdates.wiz.cancel(); + }, + + onExtra2: function() { + gUpdates.never(); + gUpdates.wiz.cancel(); + } +}; + +/** + * The "Update is Downloading" page - provides feedback for the download + * process plus a pause/resume UI + */ +var gDownloadingPage = { + /** + * DOM Elements + */ + _downloadStatus: null, + _downloadProgress: null, + _pauseButton: null, + + /** + * Whether or not we are currently paused + */ + _paused: false, + + /** + * Label cache to hold the 'Connecting' string + */ + _label_downloadStatus: null, + + /** + * Member variables for updating download status + */ + _lastSec: Infinity, + _startTime: null, + _pausedStatus: "", + + _hiding: false, + + /** + * Have we registered an observer for a background update being staged + */ + _updateApplyingObserver: false, + + /** + * Initialize + */ + onPageShow: function() { + this._downloadStatus = document.getElementById("downloadStatus"); + this._downloadProgress = document.getElementById("downloadProgress"); + this._pauseButton = document.getElementById("pauseButton"); + this._label_downloadStatus = this._downloadStatus.textContent; + + this._pauseButton.setAttribute("tooltiptext", + gUpdates.getAUSString("pauseButtonPause")); + + // move focus to the pause/resume button and then disable it (bug #353177) + this._pauseButton.focus(); + this._pauseButton.disabled = true; + + var aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + + var um = CoC["@mozilla.org/updates/update-manager;1"]. + getService(CoI.nsIUpdateManager); + var activeUpdate = um.activeUpdate; + if (activeUpdate) { + gUpdates.setUpdate(activeUpdate); + + // It's possible the update has already been downloaded and is being + // applied by the time this page is shown, depending on how fast the + // download goes and how quickly the 'next' button is clicked to get here. + if (activeUpdate.state == STATE_PENDING || + activeUpdate.state == STATE_PENDING_ELEVATE || + activeUpdate.state == STATE_PENDING_SERVICE) { + if (!activeUpdate.getProperty("stagingFailed")) { + gUpdates.setButtons("hideButton", null, null, false); + gUpdates.wiz.getButton("extra1").focus(); + + this._setUpdateApplying(); + return; + } + + gUpdates.wiz.goTo("finished"); + return; + } + } + + if (!gUpdates.update) { + LOG("gDownloadingPage", "onPageShow - no valid update to download?!"); + return; + } + + this._startTime = Date.now(); + + try { + // Say that this was a foreground download, not a background download, + // since the user cared enough to look in on this process. + gUpdates.update.QueryInterface(CoI.nsIWritablePropertyBag); + gUpdates.update.setProperty("foregroundDownload", "true"); + + // Pause any active background download and restart it as a foreground + // download. + aus.pauseDownload(); + var state = aus.downloadUpdate(gUpdates.update, false); + if (state == "failed") { + // We've tried as hard as we could to download a valid update - + // we fell back from a partial patch to a complete patch and even + // then we couldn't validate. Show a validation error with instructions + // on how to manually update. + this.cleanUp(); + gUpdates.wiz.goTo("errors"); + return; + } + // Add this UI as a listener for active downloads + aus.addDownloadListener(this); + + if (activeUpdate) + this._setUIState(!aus.isDownloading); + } + catch (e) { + LOG("gDownloadingPage", "onPageShow - error: " + e); + } + + gUpdates.setButtons("hideButton", null, null, false); + gUpdates.wiz.getButton("extra1").focus(); + }, + + /** + * Updates the text status message + */ + _setStatus: function(status) { + // Don't bother setting the same text more than once. This can happen + // due to the asynchronous behavior of the downloader. + if (this._downloadStatus.textContent == status) + return; + while (this._downloadStatus.hasChildNodes()) + this._downloadStatus.removeChild(this._downloadStatus.firstChild); + this._downloadStatus.appendChild(document.createTextNode(status)); + }, + + /** + * Update download progress status to show time left, speed, and progress. + * Also updates the status needed for pausing the download. + * + * @param aCurr + * Current number of bytes transferred + * @param aMax + * Total file size of the download + * @return Current active download status + */ + _updateDownloadStatus: function(aCurr, aMax) { + let status; + + // Get the download time left and progress + let rate = aCurr / (Date.now() - this._startTime) * 1000; + [status, this._lastSec] = + DownloadUtils.getDownloadStatus(aCurr, aMax, rate, this._lastSec); + + // Get the download progress for pausing + this._pausedStatus = DownloadUtils.getTransferTotal(aCurr, aMax); + + return status; + }, + + /** + * Adjust UI to suit a certain state of paused-ness + * @param paused + * Whether or not the download is paused + */ + _setUIState: function(paused) { + var u = gUpdates.update; + if (paused) { + if (this._downloadProgress.mode != "normal") + this._downloadProgress.mode = "normal"; + this._pauseButton.setAttribute("tooltiptext", + gUpdates.getAUSString("pauseButtonResume")); + this._pauseButton.setAttribute("paused", "true"); + var p = u.selectedPatch.QueryInterface(CoI.nsIPropertyBag); + var status = p.getProperty("status"); + if (status) { + let pausedStatus = gUpdates.getAUSString("downloadPausedStatus", [status]); + this._setStatus(pausedStatus); + } + } + else { + if (this._downloadProgress.mode != "undetermined") + this._downloadProgress.mode = "undetermined"; + this._pauseButton.setAttribute("paused", "false"); + this._pauseButton.setAttribute("tooltiptext", + gUpdates.getAUSString("pauseButtonPause")); + this._setStatus(this._label_downloadStatus); + } + }, + + /** + * Wait for an update being staged in the background. + */ + _setUpdateApplying: function() { + this._downloadProgress.mode = "undetermined"; + this._pauseButton.hidden = true; + let applyingStatus = gUpdates.getAUSString("applyingUpdate"); + this._setStatus(applyingStatus); + + Services.obs.addObserver(this, "update-staged", false); + this._updateApplyingObserver = true; + }, + + /** + * Clean up the listener and observer registered for the wizard. + */ + cleanUp: function() { + var aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + aus.removeDownloadListener(this); + + if (this._updateApplyingObserver) { + Services.obs.removeObserver(this, "update-staged"); + this._updateApplyingObserver = false; + } + }, + + /** + * When the user clicks the Pause/Resume button + */ + onPause: function() { + var aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (this._paused) + aus.downloadUpdate(gUpdates.update, false); + else { + var patch = gUpdates.update.selectedPatch; + patch.QueryInterface(CoI.nsIWritablePropertyBag); + patch.setProperty("status", this._pausedStatus); + aus.pauseDownload(); + } + this._paused = !this._paused; + + // Update the UI + this._setUIState(this._paused); + }, + + /** + * When the user has closed the window using a Window Manager control (this + * page doesn't have a cancel button) cancel the update in progress. + */ + onWizardCancel: function() { + if (this._hiding) + return; + + this.cleanUp(); + }, + + /** + * When the user closes the Wizard UI by clicking the Hide button + */ + onHide: function() { + // Set _hiding to true to prevent onWizardCancel from cancelling the update + // that is in progress. + this._hiding = true; + + // Remove ourself as a download listener so that we don't continue to be + // fed progress and state notifications after the UI we're updating has + // gone away. + this.cleanUp(); + + var aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + var um = CoC["@mozilla.org/updates/update-manager;1"]. + getService(CoI.nsIUpdateManager); + um.activeUpdate = gUpdates.update; + + // If the download was paused by the user, ask the user if they want to + // have the update resume in the background. + var downloadInBackground = true; + if (this._paused) { + var title = gUpdates.getAUSString("resumePausedAfterCloseTitle"); + var message = gUpdates.getAUSString("resumePausedAfterCloseMsg", + [gUpdates.brandName]); + var ps = Services.prompt; + var flags = ps.STD_YES_NO_BUTTONS; + // Focus the software update wizard before prompting. This will raise + // the software update wizard if it is minimized making it more obvious + // what the prompt is for and will solve the problem of windows + // obscuring the prompt. See bug #350299 for more details. + window.focus(); + var rv = ps.confirmEx(window, title, message, flags, null, null, null, + null, { }); + if (rv == CoI.nsIPromptService.BUTTON_POS_0) + downloadInBackground = false; + } + if (downloadInBackground) { + // Continue download in the background at full speed. + LOG("gDownloadingPage", "onHide - continuing download in background " + + "at full speed"); + aus.downloadUpdate(gUpdates.update, false); + } + gUpdates.wiz.cancel(); + }, + + /** + * When the data transfer begins + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + */ + onStartRequest: function(request, context) { + // This !paused test is necessary because onStartRequest may fire after + // the download was paused (for those speedy clickers...) + if (this._paused) + return; + + if (this._downloadProgress.mode != "undetermined") + this._downloadProgress.mode = "undetermined"; + this._setStatus(this._label_downloadStatus); + }, + + /** + * When new data has been downloaded + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + * @param progress + * The current number of bytes transferred + * @param maxProgress + * The total number of bytes that must be transferred + */ + onProgress: function(request, context, progress, maxProgress) { + let status = this._updateDownloadStatus(progress, maxProgress); + var currentProgress = Math.round(100 * (progress / maxProgress)); + + var p = gUpdates.update.selectedPatch; + p.QueryInterface(CoI.nsIWritablePropertyBag); + p.setProperty("progress", currentProgress); + p.setProperty("status", status); + + // This !paused test is necessary because onProgress may fire after + // the download was paused (for those speedy clickers...) + if (this._paused) + return; + + if (this._downloadProgress.mode != "normal") + this._downloadProgress.mode = "normal"; + if (this._downloadProgress.value != currentProgress) + this._downloadProgress.value = currentProgress; + if (this._pauseButton.disabled) + this._pauseButton.disabled = false; + + // If the update has completed downloading and the download status contains + // the original text return early to avoid an assertion in debug builds. + // Since the page will advance immmediately due to the update completing the + // download updating the status is not important. + // nsTextFrame::GetTrimmedOffsets 'Can only call this on frames that have + // been reflowed'. + if (progress == maxProgress && + this._downloadStatus.textContent == this._label_downloadStatus) + return; + + this._setStatus(status); + }, + + /** + * When we have new status text + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + * @param status + * A status code + * @param statusText + * Human readable version of |status| + */ + onStatus: function(request, context, status, statusText) { + this._setStatus(statusText); + }, + + /** + * When data transfer ceases + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + * @param status + * Status code containing the reason for the cessation. + */ + onStopRequest: function(request, context, status) { + if (this._downloadProgress.mode != "normal") + this._downloadProgress.mode = "normal"; + + var u = gUpdates.update; + switch (status) { + case CoR.NS_ERROR_CORRUPTED_CONTENT: + case CoR.NS_ERROR_UNEXPECTED: + if (u.selectedPatch.state == STATE_DOWNLOAD_FAILED && + (u.isCompleteUpdate || u.patchCount != 2)) { + // Verification error of complete patch, informational text is held in + // the update object. + this.cleanUp(); + gUpdates.wiz.goTo("errors"); + break; + } + // Verification failed for a partial patch, complete patch is now + // downloading so return early and do NOT remove the download listener! + + // Reset the progress meter to "undertermined" mode so that we don't + // show old progress for the new download of the "complete" patch. + this._downloadProgress.mode = "undetermined"; + this._pauseButton.disabled = true; + document.getElementById("verificationFailed").hidden = false; + break; + case CoR.NS_BINDING_ABORTED: + LOG("gDownloadingPage", "onStopRequest - pausing download"); + // Do not remove UI listener since the user may resume downloading again. + break; + case CoR.NS_OK: + LOG("gDownloadingPage", "onStopRequest - patch verification succeeded"); + // If the background update pref is set, we should wait until the update + // is actually staged in the background. + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (aus.canStageUpdates) { + this._setUpdateApplying(); + } else { + this.cleanUp(); + gUpdates.wiz.goTo("finished"); + } + break; + default: + LOG("gDownloadingPage", "onStopRequest - transfer failed"); + // Some kind of transfer error, die. + this.cleanUp(); + gUpdates.wiz.goTo("errors"); + break; + } + }, + + /** + * See nsIObserver.idl + */ + observe: function(aSubject, aTopic, aData) { + if (aTopic == "update-staged") { + if (aData == STATE_DOWNLOADING) { + // We've fallen back to downloding the full update because the + // partial update failed to get staged in the background. + this._setStatus("downloading"); + return; + } + this.cleanUp(); + if (aData == STATE_APPLIED || + aData == STATE_APPLIED_SERVICE || + aData == STATE_PENDING || + aData == STATE_PENDING_SERVICE || + aData == STATE_PENDING_ELEVATE) { + // If the update is successfully applied, or if the updater has + // fallen back to non-staged updates, go to the finish page. + gUpdates.wiz.goTo("finished"); + } else { + gUpdates.wiz.goTo("errors"); + } + } + }, + + /** + * See nsISupports.idl + */ + QueryInterface: function(iid) { + if (!iid.equals(CoI.nsIRequestObserver) && + !iid.equals(CoI.nsIProgressEventSink) && + !iid.equals(CoI.nsIObserver) && + !iid.equals(CoI.nsISupports)) + throw CoR.NS_ERROR_NO_INTERFACE; + return this; + } +}; + +/** + * The "There was an error during the update" page. + */ +var gErrorsPage = { + /** + * Initialize + */ + onPageShow: function() { + gUpdates.setButtons(null, null, "okButton", true); + gUpdates.wiz.getButton("finish").focus(); + + var statusText = gUpdates.update.statusText; + LOG("gErrorsPage", "onPageShow - update.statusText: " + statusText); + + var errorReason = document.getElementById("errorReason"); + errorReason.value = statusText; + var manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_MANUAL); + var errorLinkLabel = document.getElementById("errorLinkLabel"); + errorLinkLabel.value = manualURL; + errorLinkLabel.setAttribute("url", manualURL); + } +}; + +/** + * The page shown when there is a background check or a certificate attribute + * error. + */ +var gErrorExtraPage = { + /** + * Initialize + */ + onPageShow: function() { + gUpdates.setButtons(null, null, "okButton", true); + gUpdates.wiz.getButton("finish").focus(); + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS); + } + + document.getElementById("genericBackgroundErrorLabel").hidden = false; + let manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_MANUAL); + let errorLinkLabel = document.getElementById("errorExtraLinkLabel"); + errorLinkLabel.value = manualURL; + errorLinkLabel.setAttribute("url", manualURL); + } +}; + +/** + * The "There was an error applying a partial patch" page. + */ +var gErrorPatchingPage = { + /** + * Initialize + */ + onPageShow: function() { + gUpdates.setButtons(null, null, "okButton", true); + }, + + onWizardNext: function() { + switch (gUpdates.update.selectedPatch.state) { + case STATE_APPLIED: + case STATE_APPLIED_SERVICE: + gUpdates.wiz.goTo("finished"); + break; + case STATE_PENDING: + case STATE_PENDING_SERVICE: + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (!aus.canStageUpdates) { + gUpdates.wiz.goTo("finished"); + break; + } + // intentional fallthrough + case STATE_DOWNLOADING: + gUpdates.wiz.goTo("downloading"); + break; + case STATE_DOWNLOAD_FAILED: + gUpdates.wiz.goTo("errors"); + break; + } + } +}; + +/** + * The "Update has been downloaded" page. Shows information about what + * was downloaded. + */ +var gFinishedPage = { + /** + * Initialize + */ + onPageShow: function() { + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (aus.elevationRequired) { + LOG("gFinishedPage", "elevationRequired"); + gUpdates.setButtons("restartLaterButton", "noThanksButton", + "restartNowButton", true); + } else { + LOG("gFinishedPage", "not elevationRequired"); + gUpdates.setButtons("restartLaterButton", null, "restartNowButton", + true); + } + gUpdates.wiz.getButton("finish").focus(); + }, + + /** + * Initialize the Wizard Page for a Background Source Event + */ + onPageShowBackground: function() { + this.onPageShow(); + let updateFinishedName = document.getElementById("updateFinishedName"); + updateFinishedName.value = gUpdates.update.name; + + let link = document.getElementById("finishedBackgroundLink"); + if (gUpdates.update.detailsURL) { + link.setAttribute("url", gUpdates.update.detailsURL); + // The details link is stealing focus so it is disabled by default and + // should only be enabled after onPageShow has been called. + link.disabled = false; + } else { + link.hidden = true; + } + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (aus.elevationRequired) { + let more = document.getElementById("finishedBackgroundMore"); + more.setAttribute("hidden", "true"); + let moreElevated = + document.getElementById("finishedBackgroundMoreElevated"); + moreElevated.setAttribute("hidden", "false"); + let moreElevatedLink = + document.getElementById("finishedBackgroundMoreElevatedLink"); + moreElevatedLink.setAttribute("hidden", "false"); + let moreElevatedLinkLabel = + document.getElementById("finishedBackgroundMoreElevatedLinkLabel"); + let manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_MANUAL); + moreElevatedLinkLabel.value = manualURL; + moreElevatedLinkLabel.setAttribute("url", manualURL); + moreElevatedLinkLabel.setAttribute("hidden", "false"); + } + + if (getPref("getBoolPref", PREF_APP_UPDATE_TEST_LOOP, false)) { + setTimeout(function () { gUpdates.wiz.getButton("finish").click(); }, + UPDATE_TEST_LOOP_INTERVAL); + } + }, + + /** + * Called when the wizard finishes, i.e. the "Restart Now" button is + * clicked. + */ + onWizardFinish: function() { + // Do the restart + LOG("gFinishedPage", "onWizardFinish - restarting the application"); + + let aus = CoC["@mozilla.org/updates/update-service;1"]. + getService(CoI.nsIApplicationUpdateService); + if (aus.elevationRequired) { + let um = CoC["@mozilla.org/updates/update-manager;1"]. + getService(CoI.nsIUpdateManager); + if (um) { + um.elevationOptedIn(); + } + } + + // disable the "finish" (Restart) and "extra1" (Later) buttons + // because the Software Update wizard is still up at the point, + // and will remain up until we return and we close the + // window with a |window.close()| in wizard.xml + // (it was the firing the "wizardfinish" event that got us here.) + // This prevents the user from switching back + // to the Software Update dialog and clicking "Restart" or "Later" + // when dealing with the "confirm close" prompts. + // See bug #350299 for more details. + gUpdates.wiz.getButton("finish").disabled = true; + gUpdates.wiz.getButton("extra1").disabled = true; + + // Notify all windows that an application quit has been requested. + var cancelQuit = CoC["@mozilla.org/supports-PRBool;1"]. + createInstance(CoI.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", + "restart"); + + // Something aborted the quit process. + if (cancelQuit.data) + return; + + // If already in safe mode restart in safe mode (bug 327119) + if (Services.appinfo.inSafeMode) { + let env = CoC["@mozilla.org/process/environment;1"]. + getService(CoI.nsIEnvironment); + env.set("MOZ_SAFE_MODE_RESTART", "1"); + } + + // Restart the application + CoC["@mozilla.org/toolkit/app-startup;1"].getService(CoI.nsIAppStartup). + quit(CoI.nsIAppStartup.eAttemptQuit | CoI.nsIAppStartup.eRestart); + }, + + /** + * When the user clicks the "Restart Later" instead of the Restart Now" button + * in the wizard after an update has been downloaded. + */ + onExtra1: function() { + gUpdates.wiz.cancel(); + }, + + /** + * When elevation is required and the user clicks "No Thanks" in the wizard. + */ + onExtra2: Task.async(function*() { + Services.obs.notifyObservers(null, "update-canceled", null); + let um = CoC["@mozilla.org/updates/update-manager;1"]. + getService(CoI.nsIUpdateManager); + um.cleanupActiveUpdate(); + gUpdates.never(); + gUpdates.wiz.cancel(); + }), +}; + +/** + * Callback for the Update Prompt to set the current page if an Update Wizard + * window is already found to be open. + * @param pageid + * The ID of the page to switch to + */ +function setCurrentPage(pageid) { + gUpdates.wiz.currentPage = document.getElementById(pageid); +} diff --git a/toolkit/mozapps/update/content/updates.xml b/toolkit/mozapps/update/content/updates.xml new file mode 100644 index 000000000..9e8d4e936 --- /dev/null +++ b/toolkit/mozapps/update/content/updates.xml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<!DOCTYPE bindings SYSTEM "chrome://mozapps/locale/update/updates.dtd"> + +<bindings id="updatesBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="updateheader" extends="chrome://global/content/bindings/wizard.xml#wizard-header"> + <resources> + <stylesheet src="chrome://global/skin/wizard.css"/> + </resources> + <content> + <xul:hbox class="wizard-header update-header" flex="1"> + <xul:vbox class="wizard-header-box-1"> + <xul:vbox class="wizard-header-box-text"> + <xul:label class="wizard-header-label" xbl:inherits="value=label"/> + </xul:vbox> + </xul:vbox> + </xul:hbox> + </content> + </binding> + + <binding id="update" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content> + <xul:hbox> + <xul:label class="update-name" xbl:inherits="value=name" flex="1" crop="right"/> + <xul:label xbl:inherits="href=detailsURL,hidden=hideDetailsURL" class="text-link" + value="&update.details.label;"/> + </xul:hbox> + <xul:label class="update-type" xbl:inherits="value=type"/> + <xul:grid> + <xul:columns> + <xul:column class="update-label-column"/> + <xul:column flex="1"/> + </xul:columns> + <xul:rows> + <xul:row> + <xul:label class="update-installedOn-label">&update.installedOn.label;</xul:label> + <xul:label class="update-installedOn-value" xbl:inherits="value=installDate" flex="1" crop="right"/> + </xul:row> + <xul:row> + <xul:label class="update-status-label">&update.status.label;</xul:label> + <xul:description class="update-status-value" flex="1"/> + </xul:row> + </xul:rows> + </xul:grid> + </content> + <implementation> + <property name="name" + onget="return this.getAttribute('name');" + onset="this.setAttribute('name', val); return val;"/> + <property name="detailsURL" + onget="return this.getAttribute('detailsURL');" + onset="this.setAttribute('detailsURL', val); return val;"/> + <property name="installDate" + onget="return this.getAttribute('installDate');" + onset="this.setAttribute('installDate', val); return val;"/> + <property name="type" + onget="return this.getAttribute('type');" + onset="this.setAttribute('type', val); return val;"/> + <property name="hideDetailsURL" + onget="return this.getAttribute('hideDetailsURL');" + onset="this.setAttribute('hideDetailsURL', val); return val;"/> + <property name="status" + onget="return this.getAttribute('status');"> + <setter><![CDATA[ + this.setAttribute("status", val); + var field = document.getAnonymousElementByAttribute(this, "class", "update-status-value"); + while (field.hasChildNodes()) + field.removeChild(field.firstChild); + field.appendChild(document.createTextNode(val)); + return val; + ]]></setter> + </property> + </implementation> + </binding> +</bindings> diff --git a/toolkit/mozapps/update/content/updates.xul b/toolkit/mozapps/update/content/updates.xul new file mode 100644 index 000000000..e4cdc7a49 --- /dev/null +++ b/toolkit/mozapps/update/content/updates.xul @@ -0,0 +1,206 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://mozapps/content/update/updates.css"?> +<?xml-stylesheet href="chrome://mozapps/skin/update/updates.css"?> + +<!DOCTYPE wizard [ +<!ENTITY % updateDTD SYSTEM "chrome://mozapps/locale/update/updates.dtd"> +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%updateDTD; +%brandDTD; +]> + +<wizard id="updates" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&updateWizard.title;" + windowtype="Update:Wizard" + style="width: auto; height: auto" + onwizardfinish="gUpdates.onWizardFinish();" + onwizardcancel="gUpdates.onWizardCancel();" + onwizardnext="gUpdates.onWizardNext();" + onload="gUpdates.onLoad();" + onunload="gUpdates.onUnload();"> + + <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> + <script type="application/javascript" src="chrome://mozapps/content/update/updates.js"/> + + <stringbundleset id="updateSet"> + <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/> + <stringbundle id="updateStrings" src="chrome://mozapps/locale/update/updates.properties"/> + </stringbundleset> + + <wizardpage id="dummy" pageid="dummy" firstpage="true"/> + + <wizardpage id="checking" pageid="checking" next="noupdatesfound" + object="gCheckingPage" onpageshow="gCheckingPage.onPageShow();"> + <updateheader label="&checking.title;"/> + <vbox class="update-content" flex="1"> + <label>&updateCheck.label;</label> + <separator class="thin"/> + <progressmeter id="checkingProgress" mode="undetermined"/> + </vbox> + </wizardpage> + + <wizardpage id="noupdatesfound" pageid="noupdatesfound" + object="gNoUpdatesPage" onpageshow="gNoUpdatesPage.onPageShow();"> + <updateheader label="&noupdatesfound.title;"/> + <vbox class="update-content" flex="1"> + <label id="noUpdatesAutoEnabled" hidden="true">&noupdatesautoenabled.intro;</label> + <label id="noUpdatesAutoDisabled" hidden="true">&noupdatesautodisabled.intro;</label> + </vbox> + </wizardpage> + + <wizardpage id="manualUpdate" pageid="manualUpdate" object="gManualUpdatePage" + onpageshow="gManualUpdatePage.onPageShow();"> + <updateheader label="&manualUpdate.title;"/> + <vbox class="update-content" flex="1"> + <label id="manualUpdateDesc">&manualUpdate.desc;</label> + <label id="manualUpdateSpaceDesc" + hidden="true">&manualUpdate.space.desc;</label> + <separator class="thin"/> + <label>&manualUpdateGetMsg.label;</label> + <hbox> + <label class="text-link" id="manualUpdateLinkLabel" value="" + onclick="openUpdateURL(event);"/> + </hbox> + </vbox> + </wizardpage> + + <wizardpage id="unsupported" pageid="unsupported" + object="gUnsupportedPage" + onpageshow="gUnsupportedPage.onPageShow();"> + <updateheader label="&unsupported.title;"/> + <vbox class="update-content" flex="1"> + <description flex="1">&unsupported.label; + <label id="unsupportedLinkLabel" class="text-link inline-link" onclick="openUpdateURL(event);"> + &unsupportedLink.label; + </label> + </description> + </vbox> + </wizardpage> + + <wizardpage id="updatesfoundbasic" pageid="updatesfoundbasic" + object="gUpdatesFoundBasicPage" next="downloading" + onpageshow="gUpdatesFoundBasicPage.onPageShow();" + onextra1="gUpdatesFoundBasicPage.onExtra1();" + onextra2="gUpdatesFoundBasicPage.onExtra2();"> + <updateheader id="updatesFoundBasicHeader" label=""/> + <vbox class="update-content" flex="1"> + <label id="updatesFoundInto"/> + <separator class="thin"/> + <label id="updateName" crop="right" value=""/> + <separator id="updateNameSep" class="thin"/> + <label id="upgradeEvangelism">&evangelism.desc;</label> + <separator id="upgradeEvangelismSep" flex="1"/> + <vbox flex="1"> + <hbox id="moreInfoURL"> + <label class="text-link" id="updateMoreInfoURL" + value="&clickHere.label;" onclick="openUpdateURL(event);"/> + </hbox> + </vbox> + </vbox> + </wizardpage> + + <wizardpage id="downloading" pageid="downloading" + object="gDownloadingPage" onextra1="gDownloadingPage.onHide();" + onpageshow="gDownloadingPage.onPageShow();"> + <updateheader label="&downloadPage.title;"/> + <vbox class="update-content" flex="1"> + <hbox id="downloadStatusProgress"> + <progressmeter id="downloadProgress" mode="undetermined" flex="1"/> + <button id="pauseButton" oncommand="gDownloadingPage.onPause();" + paused="false"/> + </hbox> + <separator class="thin"/> + <hbox id="downloadStatusLine"> + <label id="downloadStatus" flex="1">&connecting.label;</label> + </hbox> + <separator/> + <hbox id="verificationFailed" align="start" hidden="true"> + <image id="verificationFailedIcon"/> + <label flex="1">&verificationFailedText.label;</label> + </hbox> + </vbox> + </wizardpage> + + <wizardpage id="errors" pageid="errors" object="gErrorsPage" + onpageshow="gErrorsPage.onPageShow();"> + <updateheader label="&error.title;"/> + <vbox class="update-content" flex="1"> + <label id="errorIntro">&error.label;</label> + <separator/> + <textbox class="plain" readonly="true" id="errorReason" multiline="true" + rows="3"/> + <separator/> + <label id="errorManual">&errorManual.label;</label> + <hbox> + <label class="text-link" id="errorLinkLabel" value="" + onclick="openUpdateURL(event);"/> + </hbox> + </vbox> + </wizardpage> + + <wizardpage id="errorextra" pageid="errorextra" + object="gErrorExtraPage" + onpageshow="gErrorExtraPage.onPageShow();"> + <updateheader label="&error.title;"/> + <vbox class="update-content" flex="1"> + <label id="bgErrorLabel">&genericBackgroundError.label;</label> + <hbox> + <label id="errorExtraLinkLabel" class="text-link" + value="" onclick="openUpdateURL(event);"/> + </hbox> + </vbox> + </wizardpage> + + <wizardpage id="errorpatching" pageid="errorpatching" next="downloading" + object="gErrorPatchingPage" + onpageshow="gErrorPatchingPage.onPageShow();"> + <updateheader label="&error.title;"/> + <vbox class="update-content" flex="1"> + <label>&errorpatching.intro;</label> + </vbox> + </wizardpage> + + <wizardpage id="finished" pageid="finished" object="gFinishedPage" + onpageshow="gFinishedPage.onPageShow();" + onextra1="gFinishedPage.onExtra1()"> + <updateheader label="&finishedPage.title;"/> + <vbox class="update-content" flex="1"> + <label>&finishedPage.text;</label> + </vbox> + </wizardpage> + + <wizardpage id="finishedBackground" pageid="finishedBackground" + object="gFinishedPage" onextra1="gFinishedPage.onExtra1()" + onextra2="gFinishedPage.onExtra2()" + onpageshow="gFinishedPage.onPageShowBackground();"> + <updateheader label="&finishedPage.title;"/> + <vbox class="update-content" flex="1"> + <label>&finishedBackgroundPage.text;</label> + <separator/> + <hbox align="center"> + <label>&finishedBackground.name;</label> + <label id="updateFinishedName" flex="1" crop="right" value=""/> + <label id="finishedBackgroundLink" class="text-link" disabled="true" + value="&details.link;" onclick="openUpdateURL(event);"/> + </hbox> + <spacer flex="1"/> + <label id="finishedBackgroundMore">&finishedBackground.more;</label> + <label id="finishedBackgroundMoreElevated" + hidden="true">&finishedBackground.moreElevated;</label> + <label id="finishedBackgroundMoreElevatedLink" + hidden="true">&errorManual.label;</label> + <hbox> + <label class="text-link" id="finishedBackgroundMoreElevatedLinkLabel" + value="" onclick="openUpdateURL(event);" hidden="true"/> + </hbox> + </vbox> + </wizardpage> + +</wizard> diff --git a/toolkit/mozapps/update/jar.mn b/toolkit/mozapps/update/jar.mn new file mode 100644 index 000000000..9bb5d2d05 --- /dev/null +++ b/toolkit/mozapps/update/jar.mn @@ -0,0 +1,12 @@ +# 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/. + +toolkit.jar: +% content mozapps %content/mozapps/ + content/mozapps/update/history.xul (content/history.xul) + content/mozapps/update/history.js (content/history.js) + content/mozapps/update/updates.css (content/updates.css) + content/mozapps/update/updates.js (content/updates.js) + content/mozapps/update/updates.xml (content/updates.xml) + content/mozapps/update/updates.xul (content/updates.xul) diff --git a/toolkit/mozapps/update/moz.build b/toolkit/mozapps/update/moz.build new file mode 100644 index 000000000..78a6996b7 --- /dev/null +++ b/toolkit/mozapps/update/moz.build @@ -0,0 +1,33 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_MODULE = 'update' + +DIRS += [ + 'common', + 'updater', +] + +XPIDL_SOURCES += [ + 'nsIUpdateService.idl', +] + +TEST_DIRS += ['tests'] + +EXTRA_COMPONENTS += [ + 'nsUpdateService.js', + 'nsUpdateService.manifest', + 'nsUpdateServiceStub.js', +] + +EXTRA_JS_MODULES += [ + 'UpdateTelemetry.jsm', +] + +JAR_MANIFESTS += ['jar.mn'] + +with Files('**'): + BUG_COMPONENT = ('Toolkit', 'Application Update') diff --git a/toolkit/mozapps/update/nsIUpdateService.idl b/toolkit/mozapps/update/nsIUpdateService.idl new file mode 100644 index 000000000..817df5a67 --- /dev/null +++ b/toolkit/mozapps/update/nsIUpdateService.idl @@ -0,0 +1,575 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" + +interface nsIDOMDocument; +interface nsIDOMElement; +interface nsIDOMWindow; +interface nsIRequest; +interface nsIRequestObserver; +interface nsISimpleEnumerator; +interface nsIFile; + +/** + * An interface that describes an object representing a patch file that can + * be downloaded and applied to a version of this application so that it + * can be updated. + */ +[scriptable, uuid(dc8fb8a9-3a53-4031-9469-2a5197ea30e7)] +interface nsIUpdatePatch : nsISupports +{ + /** + * The type of this patch: + * "partial" A binary difference between two application versions + * "complete" A complete patch containing all of the replacement files + * to update to the new version + */ + attribute AString type; + + /** + * The URL this patch was being downloaded from + */ + attribute AString URL; + + /** + * The final URL this patch was being downloaded from + */ + attribute AString finalURL; + + /** + * The hash function to use when determining this file's integrity + */ + attribute AString hashFunction; + + /** + * The value of the hash function named above that should be computed if + * this file is not corrupt. + */ + attribute AString hashValue; + + /** + * The size of this file, in bytes. + */ + attribute unsigned long size; + + /** + * The state of this patch + */ + attribute AString state; + + /** + * true if this patch is currently selected as the patch to be downloaded and + * installed for this update transaction, false if another patch from this + * update has been selected. + */ + attribute boolean selected; + + /** + * Serializes this patch object into a DOM Element + * @param updates + * The document to serialize into + * @returns The DOM Element created by the serialization process + */ + nsIDOMElement serialize(in nsIDOMDocument updates); +}; + +/** + * An interface that describes an object representing an available update to + * the current application - this update may have several available patches + * from which one must be selected to download and install, for example we + * might select a binary difference patch first and attempt to apply that, + * then if the application process fails fall back to downloading a complete + * file-replace patch. This object also contains information about the update + * that the front end and other application services can use to learn more + * about what is going on. + */ +[scriptable, uuid(e094c045-f4ff-41fd-92da-cd2effd2c7c9)] +interface nsIUpdate : nsISupports +{ + /** + * The type of update: + * "major" A major new version of the Application + * "minor" A minor update to the Application (e.g. security update) + */ + attribute AString type; + + /** + * The name of the update, or "<Application Name> <Update Version>" + */ + attribute AString name; + + /** + * The string to display in the user interface for the version. If you want + * a real version number use appVersion. + */ + attribute AString displayVersion; + + /** + * The Application version of this update. + */ + attribute AString appVersion; + + /** + * The Application version prior to the application being updated. + */ + attribute AString previousAppVersion; + + /** + * The Build ID of this update. Used to determine a particular build, down + * to the hour, minute and second of its creation. This allows the system + * to differentiate between several nightly builds with the same |version| + * for example. + */ + attribute AString buildID; + + /** + * The URL to a page which offers details about the content of this + * update. Ideally, this page is not the release notes but some other page + * that summarizes the differences between this update and the previous, + * which also links to the release notes. + */ + attribute AString detailsURL; + + /** + * The URL to the Update Service that supplied this update. + */ + attribute AString serviceURL; + + /** + * The channel used to retrieve this update from the Update Service. + */ + attribute AString channel; + + /** + * Whether to show the update prompt which requires user confirmation when an + * update is found during a background update check. This overrides the + * default setting to download the update in the background. + */ + attribute boolean showPrompt; + + /** + * Whether to show the "No Thanks" button in the update prompt. This allows + * the user to never receive a notification for that specific update version + * again. + */ + attribute boolean showNeverForVersion; + + /** + * Whether the update is no longer supported on this system. + */ + attribute boolean unsupported; + + /** + * Allows overriding the default amount of time in seconds before prompting the + * user to apply an update. If not specified, the value of + * app.update.promptWaitTime will be used. + */ + attribute long long promptWaitTime; + + /** + * Whether or not the update being downloaded is a complete replacement of + * the user's existing installation or a patch representing the difference + * between the new version and the previous version. + */ + attribute boolean isCompleteUpdate; + + /** + * Whether or not the update is a security update or not. If this is true, + * then we present more serious sounding user interface messages to the + * user. + */ + attribute boolean isSecurityUpdate; + + /** + * Whether or not the update being downloaded is an OS update. This is + * generally only possible in Gonk right now. + */ + attribute boolean isOSUpdate; + + /** + * When the update was installed. + */ + attribute long long installDate; + + /** + * A message associated with this update, if any. + */ + attribute AString statusText; + + /** + * The currently selected patch for this update. + */ + readonly attribute nsIUpdatePatch selectedPatch; + + /** + * The state of the selected patch: + * "downloading" The update is being downloaded. + * "pending" The update is ready to be applied. + * "pending-service" The update is ready to be applied with the service. + * "pending-elevate" The update is ready to be applied but requires elevation. + * "applying" The update is being applied. + * "applied" The update is ready to be switched to. + * "applied-os" The update is OS update and to be installed. + * "applied-service" The update is ready to be switched to with the service. + * "succeeded" The update was successfully applied. + * "download-failed" The update failed to be downloaded. + * "failed" The update failed to be applied. + */ + attribute AString state; + + /** + * A numeric error code that conveys additional information about the state + * of a failed update or failed certificate attribute check during an update + * check. If the update is not in the "failed" state or the certificate + * attribute check has not failed the value is zero. + * + * TODO: Define typical error codes (for now, see updater/errors.h and the + * CERT_ATTR_CHECK_FAILED_* values in nsUpdateService.js) + */ + attribute long errorCode; + + /** + * Whether an elevation failure has been encountered for this update. + */ + attribute boolean elevationFailure; + + /** + * The number of patches supplied by this update. + */ + readonly attribute unsigned long patchCount; + + /** + * Retrieves a patch. + * @param index + * The index of the patch to retrieve. + * @returns The nsIUpdatePatch at the specified index. + */ + nsIUpdatePatch getPatchAt(in unsigned long index); + + /** + * Serializes this update object into a DOM Element + * @param updates + * The document to serialize into + * @returns The DOM Element created by the serialization process + */ + nsIDOMElement serialize(in nsIDOMDocument updates); +}; + +/** + * An interface describing an object that listens to the progress of an update + * check operation. This object is notified as the check continues, finishes + * and if it has an error. + */ +[scriptable, uuid(4aa2b4bb-39ea-407b-98ff-89f19134d4c0)] +interface nsIUpdateCheckListener : nsISupports +{ + /** + * The update check was completed. + * @param request + * The XMLHttpRequest handling the update check. + * @param updates + * An array of nsIUpdate objects listing available updates. + * @param updateCount + * The size of the |updates| array. + */ + void onCheckComplete(in jsval request, + [array, size_is(updateCount)] in nsIUpdate updates, + in unsigned long updateCount); + + /** + * An error occurred while loading the remote update service file. + * @param request + * The XMLHttpRequest handling the update check. + * @param update + * A nsIUpdate object that contains details about the + * error in its |statusText| property. + */ + void onError(in jsval request, + in nsIUpdate update); +}; + +/** + * An interface describing an object that knows how to check for updates. + */ +[scriptable, uuid(877ace25-8bc5-452a-8586-9c1cf2871994)] +interface nsIUpdateChecker : nsISupports +{ + /** + * Checks for available updates, notifying a listener of the results. + * @param listener + * An object implementing nsIUpdateCheckListener which is notified + * of the results of an update check. + * @param force + * Forces the checker to check for updates, regardless of the + * current value of the user's update settings. This is used by + * any piece of UI that offers the user the imperative option to + * check for updates now, regardless of their update settings. + * force will not work if the system administrator has locked + * the app.update.enabled preference. + */ + void checkForUpdates(in nsIUpdateCheckListener listener, in boolean force); + + /** + * Constants for the |stopChecking| function that tell the Checker how long + * to stop checking: + * + * CURRENT_CHECK: Stops the current (active) check only + * CURRENT_SESSION: Stops all checking for the current session + * ANY_CHECKS: Stops all checking, any session from now on + * (disables update checking preferences) + */ + const unsigned short CURRENT_CHECK = 1; + const unsigned short CURRENT_SESSION = 2; + const unsigned short ANY_CHECKS = 3; + + /** + * Ends any pending update check. + * @param duration + * A value representing the set of checks to stop doing. + */ + void stopChecking(in unsigned short duration); +}; + +/** + * An interface describing a global application service that handles performing + * background update checks and provides utilities for selecting and + * downloading update patches. + */ +[scriptable, uuid(1107d207-a263-403a-b268-05772ec10757)] +interface nsIApplicationUpdateService : nsISupports +{ + /** + * Checks for available updates in the background using the listener provided + * by the application update service for background checks. + */ + void checkForBackgroundUpdates(); + + /** + * The Update Checker used for background update checking. + */ + readonly attribute nsIUpdateChecker backgroundChecker; + + /** + * Selects the best update to install from a list of available updates. + * @param updates + * An array of updates that are available + * @param updateCount + * The length of the |updates| array + */ + nsIUpdate selectUpdate([array, size_is(updateCount)] in nsIUpdate updates, + in unsigned long updateCount); + + /** + * Adds a listener that receives progress and state information about the + * update that is currently being downloaded, e.g. to update a user + * interface. + * @param listener + * An object implementing nsIRequestObserver and optionally + * nsIProgressEventSink that is to be notified of state and + * progress information as the update is downloaded. + */ + void addDownloadListener(in nsIRequestObserver listener); + + /** + * Removes a listener that is receiving progress and state information + * about the update that is currently being downloaded. + * @param listener + * The listener object to remove. + */ + void removeDownloadListener(in nsIRequestObserver listener); + + /** + * + */ + AString downloadUpdate(in nsIUpdate update, in boolean background); + + /** + * Apply the OS update which has been downloaded and staged as applied. + * @param update + * The update has been downloaded and staged as applied. + * @throws if the update object is not an OS update. + */ + void applyOsUpdate(in nsIUpdate update); + + /** + * Get the Active Updates directory + * @returns An nsIFile for the active updates directory. + */ + nsIFile getUpdatesDirectory(); + + /** + * Pauses the active update download process + */ + void pauseDownload(); + + /** + * Whether or not there is an download happening at the moment. + */ + readonly attribute boolean isDownloading; + + /** + * Whether or not the Update Service can check for updates. This is a function + * of whether or not application update is disabled by the application and the + * platform the application is running on. + */ + readonly attribute boolean canCheckForUpdates; + + /** + * Whether or not the installation requires elevation. Currently only + * implemented on OSX, returns false on other platforms. + */ + readonly attribute boolean elevationRequired; + + /** + * Whether or not the Update Service can download and install updates. + * On Windows, this is a function of whether or not the maintenance service + * is installed and enabled. On other systems, and as a fallback on Windows, + * this depends on whether the current user has write access to the install + * directory. + */ + readonly attribute boolean canApplyUpdates; + + /** + * Whether or not a different instance is handling updates of this + * installation. This currently only ever returns true on Windows + * when 2 instances of an application are open. Only one of the instances + * will actually handle updates for the installation. + */ + readonly attribute boolean isOtherInstanceHandlingUpdates; + + /** + * Whether the Update Service is able to stage updates. + */ + readonly attribute boolean canStageUpdates; +}; + +/** + * An interface describing a component which handles the job of processing + * an update after it's been downloaded. + */ +[scriptable, uuid(74439497-d796-4915-8cef-3dfe43027e4d)] +interface nsIUpdateProcessor : nsISupports +{ + /** + * Processes the update which has been downloaded. + * This happens without restarting the application. + * On Windows, this can also be used for switching to an applied + * update request. + * @param update The update being applied, or null if this is a switch + * to updated application request. Must be non-null on GONK. + */ + void processUpdate(in nsIUpdate update); +}; + +/** + * An interface describing a global application service that maintains a list + * of updates previously performed as well as the current active update. + */ +[scriptable, uuid(0f1098e9-a447-4af9-b030-6f8f35c85f89)] +interface nsIUpdateManager : nsISupports +{ + /** + * Gets the update at the specified index + * @param index + * The index within the updates array + * @returns The nsIUpdate object at the specified index + */ + nsIUpdate getUpdateAt(in long index); + + /** + * Gets the total number of updates in the history list. + */ + readonly attribute long updateCount; + + /** + * The active (current) update. The active update is not in the history list. + */ + attribute nsIUpdate activeUpdate; + + /** + * Saves all updates to disk. + */ + void saveUpdates(); + + /** + * Refresh the update status based on the information in update.status. + */ + void refreshUpdateStatus(); + + /** + * The user agreed to proceed with an elevated update and we are now + * permitted to show an elevation prompt. + */ + void elevationOptedIn(); + + /** + * Clean up and remove the active update without applying it. + */ + void cleanupActiveUpdate(); +}; + +/** + * An interface describing an object that can show various kinds of Update + * notification UI to the user. + */ +[scriptable, uuid(cee3bd60-c564-42ff-a2bf-d442cb15f75c)] +interface nsIUpdatePrompt : nsISupports +{ + /** + * Shows the application update checking user interface and checks if there + * is an update available. + */ + void checkForUpdates(); + + /** + * Shows the application update available user interface advising that an + * update is available for download and install. If the app.update.silent + * preference is true or the user interface is already displayed the call will + * be a no-op. + * @param update + * The nsIUpdate object to be downloaded and installed + */ + void showUpdateAvailable(in nsIUpdate update); + + /** + * Shows the application update downloaded user interface advising that an + * update has now been downloaded and a restart is necessary to complete the + * update. If background is true (e.g. the download was not user initiated) + * and the app.update.silent preference is true the call will be a no-op. + * @param update + * The nsIUpdate object that was downloaded + * @param background + * Less obtrusive UI, starting with a non-modal notification alert + */ + void showUpdateDownloaded(in nsIUpdate update, + [optional] in boolean background); + + /** + * Shows the application update error user interface advising that an error + * occurred while checking for or applying an update. If the app.update.silent + * preference is true the call will be a no-op. + * @param update + * An nsIUpdate object representing the update that could not be + * installed. The nsIUpdate object will not be the actual update when + * the error occurred during an update check and will instead be an + * nsIUpdate object with the error information for the update check. + */ + void showUpdateError(in nsIUpdate update); + + /** + * Shows a list of all updates installed to date. + * @param parent + * An nsIDOMWindow to set as the parent for this window. Can be null. + */ + void showUpdateHistory(in nsIDOMWindow parent); + + /** + * Shows the application update downloaded user interface advising that an + * update, which requires elevation, has now been downloaded and a restart is + * necessary to complete the update. + */ + void showUpdateElevationRequired(); +}; diff --git a/toolkit/mozapps/update/nsUpdateService.js b/toolkit/mozapps/update/nsUpdateService.js new file mode 100644 index 000000000..10eb1d100 --- /dev/null +++ b/toolkit/mozapps/update/nsUpdateService.js @@ -0,0 +1,4567 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +'use strict'; + +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/FileUtils.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/ctypes.jsm", this); +Cu.import("resource://gre/modules/UpdateTelemetry.jsm", this); +Cu.import("resource://gre/modules/AppConstants.jsm", this); +Cu.importGlobalProperties(["XMLHttpRequest"]); + +const UPDATESERVICE_CID = Components.ID("{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}"); +const UPDATESERVICE_CONTRACTID = "@mozilla.org/updates/update-service;1"; + +const PREF_APP_UPDATE_ALTWINDOWTYPE = "app.update.altwindowtype"; +const PREF_APP_UPDATE_AUTO = "app.update.auto"; +const PREF_APP_UPDATE_BACKGROUNDINTERVAL = "app.update.download.backgroundInterval"; +const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors"; +const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors"; +const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations"; +const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx"; +const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max"; +const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never"; +const PREF_APP_UPDATE_ELEVATE_VERSION = "app.update.elevate.version"; +const PREF_APP_UPDATE_ENABLED = "app.update.enabled"; +const PREF_APP_UPDATE_IDLETIME = "app.update.idletime"; +const PREF_APP_UPDATE_LOG = "app.update.log"; +const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported"; +const PREF_APP_UPDATE_POSTUPDATE = "app.update.postupdate"; +const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime"; +const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled"; +const PREF_APP_UPDATE_SERVICE_ERRORS = "app.update.service.errors"; +const PREF_APP_UPDATE_SERVICE_MAXERRORS = "app.update.service.maxErrors"; +const PREF_APP_UPDATE_SILENT = "app.update.silent"; +const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors"; +const PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT = "app.update.socket.retryTimeout"; +const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled"; +const PREF_APP_UPDATE_URL = "app.update.url"; +const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details"; + +const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never."; + +const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties"; +const URI_UPDATE_HISTORY_DIALOG = "chrome://mozapps/content/update/history.xul"; +const URI_UPDATE_NS = "http://www.mozilla.org/2005/app-update"; +const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul"; +const URI_UPDATES_PROPERTIES = "chrome://mozapps/locale/update/updates.properties"; + +const KEY_UPDROOT = "UpdRootD"; +const KEY_EXECUTABLE = "XREExeF"; +// Gonk only +const KEY_UPDATE_ARCHIVE_DIR = "UpdArchD"; + +const DIR_UPDATES = "updates"; + +const FILE_ACTIVE_UPDATE_XML = "active-update.xml"; +const FILE_BACKUP_UPDATE_LOG = "backup-update.log"; +const FILE_LAST_UPDATE_LOG = "last-update.log"; +const FILE_UPDATES_XML = "updates.xml"; +const FILE_UPDATE_LINK = "update.link"; +const FILE_UPDATE_LOG = "update.log"; +const FILE_UPDATE_MAR = "update.mar"; +const FILE_UPDATE_STATUS = "update.status"; +const FILE_UPDATE_TEST = "update.test"; +const FILE_UPDATE_VERSION = "update.version"; + +const STATE_NONE = "null"; +const STATE_DOWNLOADING = "downloading"; +const STATE_PENDING = "pending"; +const STATE_PENDING_SERVICE = "pending-service"; +const STATE_PENDING_ELEVATE = "pending-elevate"; +const STATE_APPLYING = "applying"; +const STATE_APPLIED = "applied"; +const STATE_APPLIED_OS = "applied-os"; +const STATE_APPLIED_SERVICE = "applied-service"; +const STATE_SUCCEEDED = "succeeded"; +const STATE_DOWNLOAD_FAILED = "download-failed"; +const STATE_FAILED = "failed"; + +// The values below used by this code are from common/errors.h +const WRITE_ERROR = 7; +const ELEVATION_CANCELED = 9; +const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24; +const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25; +const SERVICE_UPDATER_SIGN_ERROR = 26; +const SERVICE_UPDATER_COMPARE_ERROR = 27; +const SERVICE_UPDATER_IDENTITY_ERROR = 28; +const SERVICE_STILL_APPLYING_ON_SUCCESS = 29; +const SERVICE_STILL_APPLYING_ON_FAILURE = 30; +const SERVICE_UPDATER_NOT_FIXED_DRIVE = 31; +const SERVICE_COULD_NOT_LOCK_UPDATER = 32; +const SERVICE_INSTALLDIR_ERROR = 33; +const WRITE_ERROR_ACCESS_DENIED = 35; +const WRITE_ERROR_CALLBACK_APP = 37; +const FILESYSTEM_MOUNT_READWRITE_ERROR = 43; +const SERVICE_COULD_NOT_COPY_UPDATER = 49; +const SERVICE_STILL_APPLYING_TERMINATED = 50; +const SERVICE_STILL_APPLYING_NO_EXIT_CODE = 51; +const WRITE_ERROR_FILE_COPY = 61; +const WRITE_ERROR_DELETE_FILE = 62; +const WRITE_ERROR_OPEN_PATCH_FILE = 63; +const WRITE_ERROR_PATCH_FILE = 64; +const WRITE_ERROR_APPLY_DIR_PATH = 65; +const WRITE_ERROR_CALLBACK_PATH = 66; +const WRITE_ERROR_FILE_ACCESS_DENIED = 67; +const WRITE_ERROR_DIR_ACCESS_DENIED = 68; +const WRITE_ERROR_DELETE_BACKUP = 69; +const WRITE_ERROR_EXTRACT = 70; + +// Array of write errors to simplify checks for write errors +const WRITE_ERRORS = [WRITE_ERROR, + WRITE_ERROR_ACCESS_DENIED, + WRITE_ERROR_CALLBACK_APP, + WRITE_ERROR_FILE_COPY, + WRITE_ERROR_DELETE_FILE, + WRITE_ERROR_OPEN_PATCH_FILE, + WRITE_ERROR_PATCH_FILE, + WRITE_ERROR_APPLY_DIR_PATH, + WRITE_ERROR_CALLBACK_PATH, + WRITE_ERROR_FILE_ACCESS_DENIED, + WRITE_ERROR_DIR_ACCESS_DENIED, + WRITE_ERROR_DELETE_BACKUP, + WRITE_ERROR_EXTRACT]; + +// Array of write errors to simplify checks for service errors +const SERVICE_ERRORS = [SERVICE_UPDATER_COULD_NOT_BE_STARTED, + SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS, + SERVICE_UPDATER_SIGN_ERROR, + SERVICE_UPDATER_COMPARE_ERROR, + SERVICE_UPDATER_IDENTITY_ERROR, + SERVICE_STILL_APPLYING_ON_SUCCESS, + SERVICE_STILL_APPLYING_ON_FAILURE, + SERVICE_UPDATER_NOT_FIXED_DRIVE, + SERVICE_COULD_NOT_LOCK_UPDATER, + SERVICE_INSTALLDIR_ERROR, + SERVICE_COULD_NOT_COPY_UPDATER, + SERVICE_STILL_APPLYING_TERMINATED, + SERVICE_STILL_APPLYING_NO_EXIT_CODE]; + +// Error codes 80 through 99 are reserved for nsUpdateService.js and are not +// defined in common/errors.h +const FOTA_GENERAL_ERROR = 80; +const FOTA_UNKNOWN_ERROR = 81; +const FOTA_FILE_OPERATION_ERROR = 82; +const FOTA_RECOVERY_ERROR = 83; +const INVALID_UPDATER_STATE_CODE = 98; +const INVALID_UPDATER_STATUS_CODE = 99; + +// Custom update error codes +const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110; +const NETWORK_ERROR_OFFLINE = 111; + +// Error codes should be < 1000. Errors above 1000 represent http status codes +const HTTP_ERROR_OFFSET = 1000; + +const DOWNLOAD_CHUNK_SIZE = 300000; // bytes +const DOWNLOAD_BACKGROUND_INTERVAL = 600; // seconds +const DOWNLOAD_FOREGROUND_INTERVAL = 0; + +const UPDATE_WINDOW_NAME = "Update:Wizard"; + +// The number of consecutive failures when updating using the service before +// setting the app.update.service.enabled preference to false. +const DEFAULT_SERVICE_MAX_ERRORS = 10; + +// The number of consecutive socket errors to allow before falling back to +// downloading a different MAR file or failing if already downloading the full. +const DEFAULT_SOCKET_MAX_ERRORS = 10; + +// The number of milliseconds to wait before retrying a connection error. +const DEFAULT_SOCKET_RETRYTIMEOUT = 2000; + +// Default maximum number of elevation cancelations per update version before +// giving up. +const DEFAULT_CANCELATIONS_OSX_MAX = 3; + +// This maps app IDs to their respective notification topic which signals when +// the application's user interface has been displayed. +const APPID_TO_TOPIC = { + // Firefox + "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "sessionstore-windows-restored", + // SeaMonkey + "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "sessionstore-windows-restored", + // Fennec + "{aa3c5121-dab2-40e2-81ca-7ea25febc110}": "sessionstore-windows-restored", + // Thunderbird + "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "mail-startup-done", + // Instantbird + "{33cb9019-c295-46dd-be21-8c4936574bee}": "xul-window-visible", +}; + +var gUpdateMutexHandle = null; + +// Gonk only +var gSDCardMountLock = null; + +// Gonk only +XPCOMUtils.defineLazyGetter(this, "gExtStorage", function aus_gExtStorage() { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + return Services.env.get("EXTERNAL_STORAGE"); +}); + +XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() { + return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false); +}); + +XPCOMUtils.defineLazyGetter(this, "gUpdateBundle", function aus_gUpdateBundle() { + return Services.strings.createBundle(URI_UPDATES_PROPERTIES); +}); + +// shared code for suppressing bad cert dialogs +XPCOMUtils.defineLazyGetter(this, "gCertUtils", function aus_gCertUtils() { + let temp = { }; + Cu.import("resource://gre/modules/CertUtils.jsm", temp); + return temp; +}); + +/** + * Tests to make sure that we can write to a given directory. + * + * @param updateTestFile a test file in the directory that needs to be tested. + * @param createDirectory whether a test directory should be created. + * @throws if we don't have right access to the directory. + */ +function testWriteAccess(updateTestFile, createDirectory) { + const NORMAL_FILE_TYPE = Ci.nsILocalFile.NORMAL_FILE_TYPE; + const DIRECTORY_TYPE = Ci.nsILocalFile.DIRECTORY_TYPE; + if (updateTestFile.exists()) + updateTestFile.remove(false); + updateTestFile.create(createDirectory ? DIRECTORY_TYPE : NORMAL_FILE_TYPE, + createDirectory ? FileUtils.PERMS_DIRECTORY : FileUtils.PERMS_FILE); + updateTestFile.remove(false); +} + +/** + * Windows only function that closes a Win32 handle. + * + * @param handle The handle to close + */ +function closeHandle(handle) { + let lib = ctypes.open("kernel32.dll"); + let CloseHandle = lib.declare("CloseHandle", + ctypes.winapi_abi, + ctypes.int32_t, /* success */ + ctypes.void_t.ptr); /* handle */ + CloseHandle(handle); + lib.close(); +} + +/** + * Windows only function that creates a mutex. + * + * @param aName + * The name for the mutex. + * @param aAllowExisting + * If false the function will close the handle and return null. + * @return The Win32 handle to the mutex. + */ +function createMutex(aName, aAllowExisting = true) { + if (AppConstants.platform != "win") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + + const INITIAL_OWN = 1; + const ERROR_ALREADY_EXISTS = 0xB7; + let lib = ctypes.open("kernel32.dll"); + let CreateMutexW = lib.declare("CreateMutexW", + ctypes.winapi_abi, + ctypes.void_t.ptr, /* return handle */ + ctypes.void_t.ptr, /* security attributes */ + ctypes.int32_t, /* initial owner */ + ctypes.char16_t.ptr); /* name */ + + let handle = CreateMutexW(null, INITIAL_OWN, aName); + let alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS; + if (handle && !handle.isNull() && !aAllowExisting && alreadyExists) { + closeHandle(handle); + handle = null; + } + lib.close(); + + if (handle && handle.isNull()) { + handle = null; + } + + return handle; +} + +/** + * Windows only function that determines a unique mutex name for the + * installation. + * + * @param aGlobal true if the function should return a global mutex. A global + * mutex is valid across different sessions + * @return Global mutex path + */ +function getPerInstallationMutexName(aGlobal = true) { + if (AppConstants.platform != "win") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + + let hasher = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + + let exeFile = Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsILocalFile); + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + var data = converter.convertToByteArray(exeFile.path.toLowerCase()); + + hasher.update(data, data.length); + return (aGlobal ? "Global\\" : "") + "MozillaUpdateMutex-" + hasher.finish(true); +} + +/** + * Whether or not the current instance has the update mutex. The update mutex + * gives protection against 2 applications from the same installation updating: + * 1) Running multiple profiles from the same installation path + * 2) Two applications running in 2 different user sessions from the same path + * + * @return true if this instance holds the update mutex + */ +function hasUpdateMutex() { + if (AppConstants.platform != "win") { + return true; + } + if (!gUpdateMutexHandle) { + gUpdateMutexHandle = createMutex(getPerInstallationMutexName(true), false); + } + return !!gUpdateMutexHandle; +} + +/** + * Determines whether or not all descendants of a directory are writeable. + * Note: Does not check the root directory itself for writeability. + * + * @return true if all descendants are writeable, false otherwise + */ +function areDirectoryEntriesWriteable(aDir) { + let items = aDir.directoryEntries; + while (items.hasMoreElements()) { + let item = items.getNext().QueryInterface(Ci.nsIFile); + if (!item.isWritable()) { + LOG("areDirectoryEntriesWriteable - unable to write to " + item.path); + return false; + } + if (item.isDirectory() && !areDirectoryEntriesWriteable(item)) { + return false; + } + } + return true; +} + +/** + * OSX only function to determine if the user requires elevation to be able to + * write to the application bundle. + * + * @return true if elevation is required, false otherwise + */ +function getElevationRequired() { + if (AppConstants.platform != "macosx") { + return false; + } + + try { + // Recursively check that the application bundle (and its descendants) can + // be written to. + LOG("getElevationRequired - recursively testing write access on " + + getInstallDirRoot().path); + if (!getInstallDirRoot().isWritable() || + !areDirectoryEntriesWriteable(getInstallDirRoot())) { + LOG("getElevationRequired - unable to write to application bundle, " + + "elevation required"); + return true; + } + } catch (ex) { + LOG("getElevationRequired - unable to write to application bundle, " + + "elevation required. Exception: " + ex); + return true; + } + LOG("getElevationRequired - able to write to application bundle, elevation " + + "not required"); + return false; +} + +/** + * Determines whether or not an update can be applied. This is always true on + * Windows when the service is used. Also, this is always true on OSX because we + * offer users the option to perform an elevated update when necessary. + * + * @return true if an update can be applied, false otherwise + */ +function getCanApplyUpdates() { + let useService = false; + if (shouldUseService()) { + // No need to perform directory write checks, the maintenance service will + // be able to write to all directories. + LOG("getCanApplyUpdates - bypass the write checks because we'll use the service"); + useService = true; + } + + if (!useService && AppConstants.platform != "macosx") { + try { + let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]); + LOG("getCanApplyUpdates - testing write access " + updateTestFile.path); + testWriteAccess(updateTestFile, false); + if (AppConstants.platform == "win") { + // Example windowsVersion: Windows XP == 5.1 + let windowsVersion = Services.sysinfo.getProperty("version"); + LOG("getCanApplyUpdates - windowsVersion = " + windowsVersion); + + /** + * For Vista, updates can be performed to a location requiring admin + * privileges by requesting elevation via the UAC prompt when launching + * updater.exe if the appDir is under the Program Files directory + * (e.g. C:\Program Files\) and UAC is turned on and we can elevate + * (e.g. user has a split token). + * + * Note: this does note attempt to handle the case where UAC is turned on + * and the installation directory is in a restricted location that + * requires admin privileges to update other than Program Files. + */ + let userCanElevate = false; + + if (parseFloat(windowsVersion) >= 6) { + try { + // KEY_UPDROOT will fail and throw an exception if + // appDir is not under the Program Files, so we rely on that + let dir = Services.dirsvc.get(KEY_UPDROOT, Ci.nsIFile); + // appDir is under Program Files, so check if the user can elevate + userCanElevate = Services.appinfo.QueryInterface(Ci.nsIWinAppHelper). + userCanElevate; + LOG("getCanApplyUpdates - on Vista, userCanElevate: " + userCanElevate); + } + catch (ex) { + // When the installation directory is not under Program Files, + // fall through to checking if write access to the + // installation directory is available. + LOG("getCanApplyUpdates - on Vista, appDir is not under Program Files"); + } + } + + /** + * On Windows, we no longer store the update under the app dir. + * + * If we are on Windows (including Vista, if we can't elevate) we need to + * to check that we can create and remove files from the actual app + * directory (like C:\Program Files\Mozilla Firefox). If we can't + * (because this user is not an adminstrator, for example) canUpdate() + * should return false. + * + * For Vista, we perform this check to enable updating the application + * when the user has write access to the installation directory under the + * following scenarios: + * 1) the installation directory is not under Program Files + * (e.g. C:\Program Files) + * 2) UAC is turned off + * 3) UAC is turned on and the user is not an admin + * (e.g. the user does not have a split token) + * 4) UAC is turned on and the user is already elevated, so they can't be + * elevated again + */ + if (!userCanElevate) { + // if we're unable to create the test file this will throw an exception. + let appDirTestFile = getAppBaseDir(); + appDirTestFile.append(FILE_UPDATE_TEST); + LOG("getCanApplyUpdates - testing write access " + appDirTestFile.path); + if (appDirTestFile.exists()) { + appDirTestFile.remove(false); + } + appDirTestFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + appDirTestFile.remove(false); + } + } + } catch (e) { + LOG("getCanApplyUpdates - unable to apply updates. Exception: " + e); + // No write privileges to install directory + return false; + } + } // if (!useService) + + LOG("getCanApplyUpdates - able to apply updates"); + return true; +} + +/** + * Whether or not the application can stage an update for the current session. + * These checks are only performed once per session due to using a lazy getter. + * + * @return true if updates can be staged for this session. + */ +XPCOMUtils.defineLazyGetter(this, "gCanStageUpdatesSession", function aus_gCSUS() { + if (getElevationRequired()) { + LOG("gCanStageUpdatesSession - unable to stage updates because elevation " + + "is required."); + return false; + } + + try { + let updateTestFile; + if (AppConstants.platform == "macosx") { + updateTestFile = getUpdateFile([FILE_UPDATE_TEST]); + } else { + updateTestFile = getInstallDirRoot(); + updateTestFile.append(FILE_UPDATE_TEST); + } + LOG("gCanStageUpdatesSession - testing write access " + + updateTestFile.path); + testWriteAccess(updateTestFile, true); + if (AppConstants.platform != "macosx") { + // On all platforms except Mac, we need to test the parent directory as + // well, as we need to be able to move files in that directory during the + // replacing step. + updateTestFile = getInstallDirRoot().parent; + updateTestFile.append(FILE_UPDATE_TEST); + LOG("gCanStageUpdatesSession - testing write access " + + updateTestFile.path); + updateTestFile.createUnique(Ci.nsILocalFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY); + updateTestFile.remove(false); + } + } catch (e) { + LOG("gCanStageUpdatesSession - unable to stage updates. Exception: " + + e); + // No write privileges + return false; + } + + LOG("gCanStageUpdatesSession - able to stage updates"); + return true; +}); + +/** + * Whether or not the application can stage an update. + * + * @return true if updates can be staged. + */ +function getCanStageUpdates() { + // If staging updates are disabled, then just bail out! + if (!getPref("getBoolPref", PREF_APP_UPDATE_STAGING_ENABLED, false)) { + LOG("getCanStageUpdates - staging updates is disabled by preference " + + PREF_APP_UPDATE_STAGING_ENABLED); + return false; + } + + if (AppConstants.platform == "win" && shouldUseService()) { + // No need to perform directory write checks, the maintenance service will + // be able to write to all directories. + LOG("getCanStageUpdates - able to stage updates using the service"); + return true; + } + + // For Gonk, the updater will remount the /system partition to move staged + // files into place. + if (AppConstants.platform == "gonk") { + LOG("getCanStageUpdates - able to stage updates because this is gonk"); + return true; + } + + if (!hasUpdateMutex()) { + LOG("getCanStageUpdates - unable to apply updates because another " + + "instance of the application is already handling updates for this " + + "installation."); + return false; + } + + return gCanStageUpdatesSession; +} + +XPCOMUtils.defineLazyGetter(this, "gCanCheckForUpdates", function aus_gCanCheckForUpdates() { + // If the administrator has disabled app update and locked the preference so + // users can't check for updates. This preference check is ok in this lazy + // getter since locked prefs don't change until the application is restarted. + var enabled = getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true); + if (!enabled && Services.prefs.prefIsLocked(PREF_APP_UPDATE_ENABLED)) { + LOG("gCanCheckForUpdates - unable to automatically check for updates, " + + "the preference is disabled and admistratively locked."); + return false; + } + + // If we don't know the binary platform we're updating, we can't update. + if (!UpdateUtils.ABI) { + LOG("gCanCheckForUpdates - unable to check for updates, unknown ABI"); + return false; + } + + // If we don't know the OS version we're updating, we can't update. + if (!UpdateUtils.OSVersion) { + LOG("gCanCheckForUpdates - unable to check for updates, unknown OS " + + "version"); + return false; + } + + LOG("gCanCheckForUpdates - able to check for updates"); + return true; +}); + +/** + * Logs a string to the error console. + * @param string + * The string to write to the error console. + */ +function LOG(string) { + if (gLogEnabled) { + dump("*** AUS:SVC " + string + "\n"); + Services.console.logStringMessage("AUS:SVC " + string); + } +} + +/** + * Gets a preference value, handling the case where there is no default. + * @param func + * The name of the preference function to call, on nsIPrefBranch + * @param preference + * The name of the preference + * @param defaultValue + * The default value to return in the event the preference has + * no setting + * @return The value of the preference, or undefined if there was no + * user or default value. + */ +function getPref(func, preference, defaultValue) { + try { + return Services.prefs[func](preference); + } + catch (e) { + } + return defaultValue; +} + +/** + * Convert a string containing binary values to hex. + */ +function binaryToHex(input) { + var result = ""; + for (var i = 0; i < input.length; ++i) { + var hex = input.charCodeAt(i).toString(16); + if (hex.length == 1) + hex = "0" + hex; + result += hex; + } + return result; +} + +/** + * Gets the specified directory at the specified hierarchy under the + * update root directory and creates it if it doesn't exist. + * @param pathArray + * An array of path components to locate beneath the directory + * specified by |key| + * @return nsIFile object for the location specified. + */ +function getUpdateDirCreate(pathArray) { + return FileUtils.getDir(KEY_UPDROOT, pathArray, true); +} + +/** + * Gets the application base directory. + * + * @return nsIFile object for the application base directory. + */ +function getAppBaseDir() { + return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent; +} + +/** + * Gets the root of the installation directory which is the application + * bundle directory on Mac OS X and the location of the application binary + * on all other platforms. + * + * @return nsIFile object for the directory + */ +function getInstallDirRoot() { + let dir = getAppBaseDir(); + if (AppConstants.platform == "macosx") { + // On Mac, we store the Updated.app directory inside the bundle directory. + dir = dir.parent.parent; + } + return dir; +} + +/** + * Gets the file at the specified hierarchy under the update root directory. + * @param pathArray + * An array of path components to locate beneath the directory + * specified by |key|. The last item in this array must be the + * leaf name of a file. + * @return nsIFile object for the file specified. The file is NOT created + * if it does not exist, however all required directories along + * the way are. + */ +function getUpdateFile(pathArray) { + let file = getUpdateDirCreate(pathArray.slice(0, -1)); + file.append(pathArray[pathArray.length - 1]); + return file; +} + +/** + * Returns human readable status text from the updates.properties bundle + * based on an error code + * @param code + * The error code to look up human readable status text for + * @param defaultCode + * The default code to look up should human readable status text + * not exist for |code| + * @return A human readable status text string + */ +function getStatusTextFromCode(code, defaultCode) { + let reason; + try { + reason = gUpdateBundle.GetStringFromName("check_error-" + code); + LOG("getStatusTextFromCode - transfer error: " + reason + ", code: " + + code); + } + catch (e) { + // Use the default reason + reason = gUpdateBundle.GetStringFromName("check_error-" + defaultCode); + LOG("getStatusTextFromCode - transfer error: " + reason + + ", default code: " + defaultCode); + } + return reason; +} + +/** + * Get the Active Updates directory + * @return The active updates directory, as a nsIFile object + */ +function getUpdatesDir() { + // Right now, we only support downloading one patch at a time, so we always + // use the same target directory. + return getUpdateDirCreate([DIR_UPDATES, "0"]); +} + +/** + * Reads the update state from the update.status file in the specified + * directory. + * @param dir + * The dir to look for an update.status file in + * @return The status value of the update. + */ +function readStatusFile(dir) { + let statusFile = dir.clone(); + statusFile.append(FILE_UPDATE_STATUS); + let status = readStringFromFile(statusFile) || STATE_NONE; + LOG("readStatusFile - status: " + status + ", path: " + statusFile.path); + return status; +} + +/** + * Writes the current update operation/state to a file in the patch + * directory, indicating to the patching system that operations need + * to be performed. + * @param dir + * The patch directory where the update.status file should be + * written. + * @param state + * The state value to write. + */ +function writeStatusFile(dir, state) { + let statusFile = dir.clone(); + statusFile.append(FILE_UPDATE_STATUS); + writeStringToFile(statusFile, state); +} + +/** + * Writes the update's application version to a file in the patch directory. If + * the update doesn't provide application version information via the + * appVersion attribute the string "null" will be written to the file. + * This value is compared during startup (in nsUpdateDriver.cpp) to determine if + * the update should be applied. Note that this won't provide protection from + * downgrade of the application for the nightly user case where the application + * version doesn't change. + * @param dir + * The patch directory where the update.version file should be + * written. + * @param version + * The version value to write. Will be the string "null" when the + * update doesn't provide the appVersion attribute in the update xml. + */ +function writeVersionFile(dir, version) { + let versionFile = dir.clone(); + versionFile.append(FILE_UPDATE_VERSION); + writeStringToFile(versionFile, version); +} + +/** + * Gonk only function that reads the link file specified in the update.link file + * in the specified directory and returns the nsIFile for the corresponding + * file. + * @param dir + * The dir to look for an update.link file in + * @return A nsIFile for the file path specified in the + * update.link file or null if the update.link file + * doesn't exist. + */ +function getFileFromUpdateLink(dir) { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + let linkFile = dir.clone(); + linkFile.append(FILE_UPDATE_LINK); + let link = readStringFromFile(linkFile); + LOG("getFileFromUpdateLink linkFile.path: " + linkFile.path + ", link: " + link); + if (!link) { + return null; + } + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(link); + return file; +} + +/** + * Gonk only function to create a link file. This allows the actual patch to + * live in a directory different from the update directory. + * @param dir + * The patch directory where the update.link file + * should be written. + * @param patchFile + * The fully qualified filename of the patchfile. + */ +function writeLinkFile(dir, patchFile) { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + let linkFile = dir.clone(); + linkFile.append(FILE_UPDATE_LINK); + writeStringToFile(linkFile, patchFile.path); + if (patchFile.path.indexOf(gExtStorage) == 0) { + // The patchfile is being stored on external storage. Try to lock it + // so that it doesn't get shared with the PC while we're downloading + // to it. + acquireSDCardMountLock(); + } +} + +/** + * Gonk only function to acquire a VolumeMountLock for the sdcard volume. + * + * This prevents the SDCard from being shared with the PC while + * we're downloading the update. + */ +function acquireSDCardMountLock() { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + let volsvc = Cc["@mozilla.org/telephony/volume-service;1"]. + getService(Ci.nsIVolumeService); + if (volsvc) { + gSDCardMountLock = volsvc.createMountLock("sdcard"); + } +} + +/** + * Gonk only function that determines if the state corresponds to an + * interrupted update. This could either be because the download was + * interrupted, or because staging the update was interrupted. + * + * @return true if the state corresponds to an interrupted + * update. + */ +function isInterruptedUpdate(status) { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + return (status == STATE_DOWNLOADING) || + (status == STATE_PENDING) || + (status == STATE_APPLYING); +} + +/** + * Releases any SDCard mount lock that we might have. + * + * This once again allows the SDCard to be shared with the PC. + */ +function releaseSDCardMountLock() { + if (AppConstants.platform != "gonk") { + throw Cr.NS_ERROR_UNEXPECTED; + } + if (gSDCardMountLock) { + gSDCardMountLock.unlock(); + gSDCardMountLock = null; + } +} + +/** + * Determines if the service should be used to attempt an update + * or not. + * + * @return true if the service should be used for updates. + */ +function shouldUseService() { + // This function will return true if the mantenance service should be used if + // all of the following conditions are met: + // 1) This build was done with the maintenance service enabled + // 2) The maintenance service is installed + // 3) The pref for using the service is enabled + // 4) The Windows version is XP Service Pack 3 or above (for SHA-2 support) + // The maintenance service requires SHA-2 support because we sign our binaries + // with a SHA-2 certificate and the certificate is verified before the binary + // is launched. + if (!AppConstants.MOZ_MAINTENANCE_SERVICE || !isServiceInstalled() || + !getPref("getBoolPref", PREF_APP_UPDATE_SERVICE_ENABLED, false) || + !AppConstants.isPlatformAndVersionAtLeast("win", "5.1") /* WinXP */) { + return false; + } + + // If it's newer than XP, then the service pack doesn't matter. + if (Services.sysinfo.getProperty("version") != "5.1") { + return true; + } + + // If the Windows version is XP, we also need to check the service pack. + // We'll return false if only < SP3 is installed, or if we can't tell. + // Check the service pack level by calling GetVersionEx via ctypes. + const BYTE = ctypes.uint8_t; + const WORD = ctypes.uint16_t; + const DWORD = ctypes.uint32_t; + const WCHAR = ctypes.char16_t; + const BOOL = ctypes.int; + // This structure is described at: + // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx + const SZCSDVERSIONLENGTH = 128; + const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', + [ + {dwOSVersionInfoSize: DWORD}, + {dwMajorVersion: DWORD}, + {dwMinorVersion: DWORD}, + {dwBuildNumber: DWORD}, + {dwPlatformId: DWORD}, + {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)}, + {wServicePackMajor: WORD}, + {wServicePackMinor: WORD}, + {wSuiteMask: WORD}, + {wProductType: BYTE}, + {wReserved: BYTE} + ]); + + let kernel32 = false; + try { + kernel32 = ctypes.open("Kernel32"); + } catch (e) { + Cu.reportError("Unable to open kernel32! " + e); + return false; + } + + if (kernel32) { + try { + try { + let GetVersionEx = kernel32.declare("GetVersionExW", + ctypes.default_abi, + BOOL, + OSVERSIONINFOEXW.ptr); + let winVer = OSVERSIONINFOEXW(); + winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size; + + if (0 !== GetVersionEx(winVer.address())) { + return winVer.wServicePackMajor >= 3; + } + Cu.reportError("Unknown failure in GetVersionEX (returned 0)"); + return false; + } catch (e) { + Cu.reportError("Error getting service pack information. Exception: " + e); + return false; + } + } finally { + kernel32.close(); + } + } + + // If the service pack check couldn't be done, assume we can't use the service. + return false; +} + +/** + * Determines if the service is is installed. + * + * @return true if the service is installed. + */ +function isServiceInstalled() { + if (AppConstants.MOZ_MAINTENANCE_SERVICE && AppConstants.platform == "win") { + let installed = 0; + try { + let wrk = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + wrk.open(wrk.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\MaintenanceService", + wrk.ACCESS_READ | wrk.WOW64_64); + installed = wrk.readIntValue("Installed"); + wrk.close(); + } catch (e) { + } + installed = installed == 1; // convert to bool + LOG("isServiceInstalled = " + installed); + return installed; + } + return false; +} + +/** + * Removes the contents of the updates patch directory and rotates the update + * logs when present. If the update.log exists in the patch directory this will + * move the last-update.log if it exists to backup-update.log in the parent + * directory of the patch directory and then move the update.log in the patch + * directory to last-update.log in the parent directory of the patch directory. + * + * @param aRemovePatchFiles (optional, defaults to true) + * When true the update's patch directory contents are removed. + */ +function cleanUpUpdatesDir(aRemovePatchFiles = true) { + let updateDir; + try { + updateDir = getUpdatesDir(); + } catch (e) { + LOG("cleanUpUpdatesDir - unable to get the updates patch directory. " + + "Exception: " + e); + return; + } + + // Preserve the last update log file for debugging purposes. + let updateLogFile = updateDir.clone(); + updateLogFile.append(FILE_UPDATE_LOG); + if (updateLogFile.exists()) { + let dir = updateDir.parent; + let logFile = dir.clone(); + logFile.append(FILE_LAST_UPDATE_LOG); + if (logFile.exists()) { + try { + logFile.moveTo(dir, FILE_BACKUP_UPDATE_LOG); + } catch (e) { + LOG("cleanUpUpdatesDir - failed to rename file " + logFile.path + + " to " + FILE_BACKUP_UPDATE_LOG); + } + } + + try { + updateLogFile.moveTo(dir, FILE_LAST_UPDATE_LOG); + } catch (e) { + LOG("cleanUpUpdatesDir - failed to rename file " + updateLogFile.path + + " to " + FILE_LAST_UPDATE_LOG); + } + } + + if (aRemovePatchFiles) { + let dirEntries = updateDir.directoryEntries; + while (dirEntries.hasMoreElements()) { + let file = dirEntries.getNext().QueryInterface(Ci.nsIFile); + if (AppConstants.platform == "gonk") { + if (file.leafName == FILE_UPDATE_LINK) { + let linkedFile = getFileFromUpdateLink(updateDir); + if (linkedFile && linkedFile.exists()) { + linkedFile.remove(false); + } + } + } + + // Now, recursively remove this file. The recursive removal is needed for + // Mac OSX because this directory will contain a copy of updater.app, + // which is itself a directory and the MozUpdater directory on platforms + // other than Windows. + try { + file.remove(true); + } catch (e) { + LOG("cleanUpUpdatesDir - failed to remove file " + file.path); + } + } + } + if (AppConstants.platform == "gonk") { + releaseSDCardMountLock(); + } +} + +/** + * Clean up updates list and the updates directory. + */ +function cleanupActiveUpdate() { + // Move the update from the Active Update list into the Past Updates list. + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + um.activeUpdate = null; + um.saveUpdates(); + + // Now trash the updates directory, since we're done with it + cleanUpUpdatesDir(); +} + +/** + * An enumeration of items in a JS array. + * @constructor + */ +function ArrayEnumerator(aItems) { + this._index = 0; + if (aItems) { + for (var i = 0; i < aItems.length; ++i) { + if (!aItems[i]) + aItems.splice(i, 1); + } + } + this._contents = aItems; +} + +ArrayEnumerator.prototype = { + _index: 0, + _contents: [], + + hasMoreElements: function ArrayEnumerator_hasMoreElements() { + return this._index < this._contents.length; + }, + + getNext: function ArrayEnumerator_getNext() { + return this._contents[this._index++]; + } +}; + +/** + * Writes a string of text to a file. A newline will be appended to the data + * written to the file. This function only works with ASCII text. + */ +function writeStringToFile(file, text) { + let fos = FileUtils.openSafeFileOutputStream(file); + text += "\n"; + fos.write(text, text.length); + FileUtils.closeSafeFileOutputStream(fos); +} + +function readStringFromInputStream(inputStream) { + var sis = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + sis.init(inputStream); + var text = sis.read(sis.available()); + sis.close(); + if (text && text[text.length - 1] == "\n") { + text = text.slice(0, -1); + } + return text; +} + +/** + * Reads a string of text from a file. A trailing newline will be removed + * before the result is returned. This function only works with ASCII text. + */ +function readStringFromFile(file) { + if (!file.exists()) { + LOG("readStringFromFile - file doesn't exist: " + file.path); + return null; + } + var fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); + return readStringFromInputStream(fis); +} + +function handleUpdateFailure(update, errorCode) { + update.errorCode = parseInt(errorCode); + if (update.errorCode == FOTA_GENERAL_ERROR || + update.errorCode == FOTA_FILE_OPERATION_ERROR || + update.errorCode == FOTA_RECOVERY_ERROR || + update.errorCode == FOTA_UNKNOWN_ERROR) { + // In the case of FOTA update errors, don't reset the state to pending. This + // causes the FOTA update path to try again, which is not necessarily what + // we want. + update.statusText = gUpdateBundle.GetStringFromName("statusFailed"); + + Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt). + showUpdateError(update); + writeStatusFile(getUpdatesDir(), STATE_FAILED + ": " + errorCode); + cleanupActiveUpdate(); + return true; + } + + // Replace with Array.prototype.includes when it has stabilized. + if (WRITE_ERRORS.indexOf(update.errorCode) != -1 || + update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR) { + Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt). + showUpdateError(update); + writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); + return true; + } + + if (update.errorCode == ELEVATION_CANCELED) { + let cancelations = getPref("getIntPref", PREF_APP_UPDATE_CANCELATIONS, 0); + cancelations++; + Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, cancelations); + if (AppConstants.platform == "macosx") { + let osxCancelations = getPref("getIntPref", + PREF_APP_UPDATE_CANCELATIONS_OSX, 0); + osxCancelations++; + Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS_OSX, + osxCancelations); + let maxCancels = getPref("getIntPref", + PREF_APP_UPDATE_CANCELATIONS_OSX_MAX, + DEFAULT_CANCELATIONS_OSX_MAX); + // Prevent the preference from setting a value greater than 5. + maxCancels = Math.min(maxCancels, 5); + if (osxCancelations >= maxCancels) { + cleanupActiveUpdate(); + } else { + writeStatusFile(getUpdatesDir(), + update.state = STATE_PENDING_ELEVATE); + } + update.statusText = gUpdateBundle.GetStringFromName("elevationFailure"); + update.QueryInterface(Ci.nsIWritablePropertyBag); + update.setProperty("patchingFailed", "elevationFailure"); + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(update); + } else { + writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); + } + return true; + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + + // Replace with Array.prototype.includes when it has stabilized. + if (SERVICE_ERRORS.indexOf(update.errorCode) != -1) { + var failCount = getPref("getIntPref", + PREF_APP_UPDATE_SERVICE_ERRORS, 0); + var maxFail = getPref("getIntPref", + PREF_APP_UPDATE_SERVICE_MAXERRORS, + DEFAULT_SERVICE_MAX_ERRORS); + // Prevent the preference from setting a value greater than 10. + maxFail = Math.min(maxFail, 10); + // As a safety, when the service reaches maximum failures, it will + // disable itself and fallback to using the normal update mechanism + // without the service. + if (failCount >= maxFail) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false); + Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS); + } else { + failCount++; + Services.prefs.setIntPref(PREF_APP_UPDATE_SERVICE_ERRORS, + failCount); + } + + writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); + return true; + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS); + } + + return false; +} + +/** + * Fall back to downloading a complete update in case an update has failed. + * + * @param update the update object that has failed to apply. + * @param postStaging true if we have just attempted to stage an update. + */ +function handleFallbackToCompleteUpdate(update, postStaging) { + cleanupActiveUpdate(); + + update.statusText = gUpdateBundle.GetStringFromName("patchApplyFailure"); + var oldType = update.selectedPatch ? update.selectedPatch.type + : "complete"; + if (update.selectedPatch && oldType == "partial" && update.patchCount == 2) { + // Partial patch application failed, try downloading the complete + // update in the background instead. + LOG("handleFallbackToCompleteUpdate - install of partial patch " + + "failed, downloading complete patch"); + var status = Cc["@mozilla.org/updates/update-service;1"]. + getService(Ci.nsIApplicationUpdateService). + downloadUpdate(update, !postStaging); + if (status == STATE_NONE) + cleanupActiveUpdate(); + } + else { + LOG("handleFallbackToCompleteUpdate - install of complete or " + + "only one patch offered failed."); + } + update.QueryInterface(Ci.nsIWritablePropertyBag); + update.setProperty("patchingFailed", oldType); +} + +function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) { + let patchType = AUSTLMY.PATCH_UNKNOWN; + if (aUpdate && aUpdate.selectedPatch && aUpdate.selectedPatch.type) { + if (aUpdate.selectedPatch.type == "complete") { + patchType = AUSTLMY.PATCH_COMPLETE; + } else if (aUpdate.selectedPatch.type == "partial") { + patchType = AUSTLMY.PATCH_PARTIAL; + } + } + + let suffix = patchType + "_" + (aStartup ? AUSTLMY.STARTUP : AUSTLMY.STAGE); + let stateCode = 0; + let parts = aStatus.split(":"); + if (parts.length > 0) { + switch (parts[0]) { + case STATE_NONE: + stateCode = 2; + break; + case STATE_DOWNLOADING: + stateCode = 3; + break; + case STATE_PENDING: + stateCode = 4; + break; + case STATE_PENDING_SERVICE: + stateCode = 5; + break; + case STATE_APPLYING: + stateCode = 6; + break; + case STATE_APPLIED: + stateCode = 7; + break; + case STATE_APPLIED_OS: + stateCode = 8; + break; + case STATE_APPLIED_SERVICE: + stateCode = 9; + break; + case STATE_SUCCEEDED: + stateCode = 10; + break; + case STATE_DOWNLOAD_FAILED: + stateCode = 11; + break; + case STATE_FAILED: + stateCode = 12; + break; + case STATE_PENDING_ELEVATE: + stateCode = 13; + break; + default: + stateCode = 1; + } + + if (parts.length > 1) { + let statusErrorCode = INVALID_UPDATER_STATE_CODE; + if (parts[0] == STATE_FAILED) { + statusErrorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE; + } + AUSTLMY.pingStatusErrorCode(suffix, statusErrorCode); + } + } + AUSTLMY.pingStateCode(suffix, stateCode); +} + +/** + * Update Patch + * @param patch + * A <patch> element to initialize this object with + * @throws if patch has a size of 0 + * @constructor + */ +function UpdatePatch(patch) { + this._properties = {}; + for (var i = 0; i < patch.attributes.length; ++i) { + var attr = patch.attributes.item(i); + attr.QueryInterface(Ci.nsIDOMAttr); + switch (attr.name) { + case "selected": + this.selected = attr.value == "true"; + break; + case "size": + if (0 == parseInt(attr.value)) { + LOG("UpdatePatch:init - 0-sized patch!"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + // fall through + default: + this[attr.name] = attr.value; + break; + } + } +} +UpdatePatch.prototype = { + /** + * See nsIUpdateService.idl + */ + serialize: function UpdatePatch_serialize(updates) { + var patch = updates.createElementNS(URI_UPDATE_NS, "patch"); + patch.setAttribute("type", this.type); + patch.setAttribute("URL", this.URL); + // finalURL is not available until after the download has started + if (this.finalURL) { + patch.setAttribute("finalURL", this.finalURL); + } + patch.setAttribute("hashFunction", this.hashFunction); + patch.setAttribute("hashValue", this.hashValue); + patch.setAttribute("size", this.size); + if (this.selected) { + patch.setAttribute("selected", this.selected); + } + patch.setAttribute("state", this.state); + + for (let p in this._properties) { + if (this._properties[p].present) { + patch.setAttribute(p, this._properties[p].data); + } + } + + return patch; + }, + + /** + * A hash of custom properties + */ + _properties: null, + + /** + * See nsIWritablePropertyBag.idl + */ + setProperty: function UpdatePatch_setProperty(name, value) { + this._properties[name] = { data: value, present: true }; + }, + + /** + * See nsIWritablePropertyBag.idl + */ + deleteProperty: function UpdatePatch_deleteProperty(name) { + if (name in this._properties) + this._properties[name].present = false; + else + throw Cr.NS_ERROR_FAILURE; + }, + + /** + * See nsIPropertyBag.idl + */ + get enumerator() { + var properties = []; + for (var p in this._properties) + properties.push(this._properties[p].data); + return new ArrayEnumerator(properties); + }, + + /** + * See nsIPropertyBag.idl + * Note: returns null instead of throwing when the property doesn't exist to + * simplify code and to silence warnings in debug builds. + */ + getProperty: function UpdatePatch_getProperty(name) { + if (name in this._properties && + this._properties[name].present) { + return this._properties[name].data; + } + return null; + }, + + /** + * Returns whether or not the update.status file for this patch exists at the + * appropriate location. + */ + get statusFileExists() { + var statusFile = getUpdatesDir(); + statusFile.append(FILE_UPDATE_STATUS); + return statusFile.exists(); + }, + + /** + * See nsIUpdateService.idl + */ + get state() { + if (this._properties.state) + return this._properties.state; + return STATE_NONE; + }, + set state(val) { + this._properties.state = val; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePatch, + Ci.nsIPropertyBag, + Ci.nsIWritablePropertyBag]) +}; + +/** + * Update + * Implements nsIUpdate + * @param update + * An <update> element to initialize this object with + * @throws if the update contains no patches + * @constructor + */ +function Update(update) { + this._properties = {}; + this._patches = []; + this.isCompleteUpdate = false; + this.isOSUpdate = false; + this.showPrompt = false; + this.showNeverForVersion = false; + this.unsupported = false; + this.channel = "default"; + this.promptWaitTime = getPref("getIntPref", PREF_APP_UPDATE_PROMPTWAITTIME, 43200); + this.backgroundInterval = getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDINTERVAL, + DOWNLOAD_BACKGROUND_INTERVAL); + + // Null <update>, assume this is a message container and do no + // further initialization + if (!update) { + return; + } + + const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; + let patch; + for (let i = 0; i < update.childNodes.length; ++i) { + let patchElement = update.childNodes.item(i); + if (patchElement.nodeType != ELEMENT_NODE || + patchElement.localName != "patch") { + continue; + } + + patchElement.QueryInterface(Ci.nsIDOMElement); + try { + patch = new UpdatePatch(patchElement); + } catch (e) { + continue; + } + this._patches.push(patch); + } + + if (this._patches.length == 0 && !update.hasAttribute("unsupported")) { + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + + // Set the installDate value with the current time. If the update has an + // installDate attribute this will be replaced with that value if it doesn't + // equal 0. + this.installDate = (new Date()).getTime(); + + for (let i = 0; i < update.attributes.length; ++i) { + var attr = update.attributes.item(i); + attr.QueryInterface(Ci.nsIDOMAttr); + if (attr.value == "undefined") { + continue; + } else if (attr.name == "detailsURL") { + this._detailsURL = attr.value; + } else if (attr.name == "installDate" && attr.value) { + let val = parseInt(attr.value); + if (val) { + this.installDate = val; + } + } else if (attr.name == "isCompleteUpdate") { + this.isCompleteUpdate = attr.value == "true"; + } else if (attr.name == "isSecurityUpdate") { + this.isSecurityUpdate = attr.value == "true"; + } else if (attr.name == "isOSUpdate") { + this.isOSUpdate = attr.value == "true"; + } else if (attr.name == "showNeverForVersion") { + this.showNeverForVersion = attr.value == "true"; + } else if (attr.name == "showPrompt") { + this.showPrompt = attr.value == "true"; + } else if (attr.name == "promptWaitTime") { + if (!isNaN(attr.value)) { + this.promptWaitTime = parseInt(attr.value); + } + } else if (attr.name == "backgroundInterval") { + if (!isNaN(attr.value)) { + this.backgroundInterval = parseInt(attr.value); + } + } else if (attr.name == "unsupported") { + this.unsupported = attr.value == "true"; + } else { + this[attr.name] = attr.value; + + switch (attr.name) { + case "appVersion": + case "buildID": + case "channel": + case "displayVersion": + case "name": + case "previousAppVersion": + case "serviceURL": + case "statusText": + case "type": + break; + default: + // Save custom attributes when serializing to the local xml file but + // don't use this method for the expected attributes which are already + // handled in serialize. + this.setProperty(attr.name, attr.value); + break; + } + } + } + + if (!this.displayVersion) { + this.displayVersion = this.appVersion; + } + + // Don't allow the background download interval to be greater than 10 minutes. + this.backgroundInterval = Math.min(this.backgroundInterval, 600); + + // The Update Name is either the string provided by the <update> element, or + // the string: "<App Name> <Update App Version>" + var name = ""; + if (update.hasAttribute("name")) { + name = update.getAttribute("name"); + } else { + var brandBundle = Services.strings.createBundle(URI_BRAND_PROPERTIES); + var appName = brandBundle.GetStringFromName("brandShortName"); + name = gUpdateBundle.formatStringFromName("updateName", + [appName, this.displayVersion], 2); + } + this.name = name; +} +Update.prototype = { + /** + * See nsIUpdateService.idl + */ + get patchCount() { + return this._patches.length; + }, + + /** + * See nsIUpdateService.idl + */ + getPatchAt: function Update_getPatchAt(index) { + return this._patches[index]; + }, + + /** + * See nsIUpdateService.idl + * + * We use a copy of the state cached on this object in |_state| only when + * there is no selected patch, i.e. in the case when we could not load + * |.activeUpdate| from the update manager for some reason but still have + * the update.status file to work with. + */ + _state: "", + set state(state) { + if (this.selectedPatch) + this.selectedPatch.state = state; + this._state = state; + return state; + }, + get state() { + if (this.selectedPatch) + return this.selectedPatch.state; + return this._state; + }, + + /** + * See nsIUpdateService.idl + */ + errorCode: 0, + + /** + * See nsIUpdateService.idl + */ + get selectedPatch() { + for (var i = 0; i < this.patchCount; ++i) { + if (this._patches[i].selected) + return this._patches[i]; + } + return null; + }, + + /** + * See nsIUpdateService.idl + */ + get detailsURL() { + if (!this._detailsURL) { + try { + // Try using a default details URL supplied by the distribution + // if the update XML does not supply one. + return Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_DETAILS); + } + catch (e) { + } + } + return this._detailsURL || ""; + }, + + /** + * See nsIUpdateService.idl + */ + serialize: function Update_serialize(updates) { + // If appVersion isn't defined just return null. This happens when cleaning + // up invalid updates (e.g. incorrect channel). + if (!this.appVersion) { + return null; + } + var update = updates.createElementNS(URI_UPDATE_NS, "update"); + update.setAttribute("appVersion", this.appVersion); + update.setAttribute("buildID", this.buildID); + update.setAttribute("channel", this.channel); + update.setAttribute("displayVersion", this.displayVersion); + update.setAttribute("installDate", this.installDate); + update.setAttribute("isCompleteUpdate", this.isCompleteUpdate); + update.setAttribute("isOSUpdate", this.isOSUpdate); + update.setAttribute("name", this.name); + update.setAttribute("serviceURL", this.serviceURL); + update.setAttribute("showNeverForVersion", this.showNeverForVersion); + update.setAttribute("showPrompt", this.showPrompt); + update.setAttribute("promptWaitTime", this.promptWaitTime); + update.setAttribute("backgroundInterval", this.backgroundInterval); + update.setAttribute("type", this.type); + + if (this.detailsURL) { + update.setAttribute("detailsURL", this.detailsURL); + } + if (this.previousAppVersion) { + update.setAttribute("previousAppVersion", this.previousAppVersion); + } + if (this.statusText) { + update.setAttribute("statusText", this.statusText); + } + if (this.unsupported) { + update.setAttribute("unsupported", this.unsupported); + } + updates.documentElement.appendChild(update); + + for (let p in this._properties) { + if (this._properties[p].present) { + update.setAttribute(p, this._properties[p].data); + } + } + + for (let i = 0; i < this.patchCount; ++i) { + update.appendChild(this.getPatchAt(i).serialize(updates)); + } + + return update; + }, + + /** + * A hash of custom properties + */ + _properties: null, + + /** + * See nsIWritablePropertyBag.idl + */ + setProperty: function Update_setProperty(name, value) { + this._properties[name] = { data: value, present: true }; + }, + + /** + * See nsIWritablePropertyBag.idl + */ + deleteProperty: function Update_deleteProperty(name) { + if (name in this._properties) + this._properties[name].present = false; + else + throw Cr.NS_ERROR_FAILURE; + }, + + /** + * See nsIPropertyBag.idl + */ + get enumerator() { + var properties = []; + for (let p in this._properties) { + properties.push(this._properties[p].data); + } + return new ArrayEnumerator(properties); + }, + + /** + * See nsIPropertyBag.idl + * Note: returns null instead of throwing when the property doesn't exist to + * simplify code and to silence warnings in debug builds. + */ + getProperty: function Update_getProperty(name) { + if (name in this._properties && this._properties[name].present) { + return this._properties[name].data; + } + return null; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdate, + Ci.nsIPropertyBag, + Ci.nsIWritablePropertyBag]) +}; + +const UpdateServiceFactory = { + _instance: null, + createInstance: function (outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this._instance == null ? this._instance = new UpdateService() : + this._instance; + } +}; + +/** + * UpdateService + * A Service for managing the discovery and installation of software updates. + * @constructor + */ +function UpdateService() { + LOG("Creating UpdateService"); + Services.obs.addObserver(this, "xpcom-shutdown", false); + Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this, false); + if (AppConstants.platform == "gonk") { + // PowerManagerService::SyncProfile (which is called for Reboot, PowerOff + // and Restart) sends the profile-change-net-teardown event. We can then + // pause the download in a similar manner to xpcom-shutdown. + Services.obs.addObserver(this, "profile-change-net-teardown", false); + } +} + +UpdateService.prototype = { + /** + * The downloader we are using to download updates. There is only ever one of + * these. + */ + _downloader: null, + + /** + * Whether or not the service registered the "online" observer. + */ + _registeredOnlineObserver: false, + + /** + * The current number of consecutive socket errors + */ + _consecutiveSocketErrors: 0, + + /** + * A timer used to retry socket errors + */ + _retryTimer: null, + + /** + * Whether or not a background update check was initiated by the + * application update timer notification. + */ + _isNotify: true, + + /** + * Handle Observer Service notifications + * @param subject + * The subject of the notification + * @param topic + * The notification name + * @param data + * Additional data + */ + observe: function AUS_observe(subject, topic, data) { + switch (topic) { + case "post-update-processing": + if (readStatusFile(getUpdatesDir()) == STATE_SUCCEEDED) { + // The active update needs to be copied to the first update in the + // updates.xml early during startup to support post update actions + // (bug 1301288). + let um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + um.activeUpdate.state = STATE_SUCCEEDED; + um.saveUpdates(); + Services.prefs.setBoolPref(PREF_APP_UPDATE_POSTUPDATE, true); + } + + if (Services.appinfo.ID in APPID_TO_TOPIC) { + // Delay post-update processing to ensure that possible update + // dialogs are shown in front of the app window, if possible. + // See bug 311614. + Services.obs.addObserver(this, APPID_TO_TOPIC[Services.appinfo.ID], + false); + break; + } + // intentional fallthrough + case "sessionstore-windows-restored": + case "mail-startup-done": + case "xul-window-visible": + if (Services.appinfo.ID in APPID_TO_TOPIC) { + Services.obs.removeObserver(this, + APPID_TO_TOPIC[Services.appinfo.ID]); + } + // intentional fallthrough + case "test-post-update-processing": + // Clean up any extant updates + this._postUpdateProcessing(); + break; + case "network:offline-status-changed": + this._offlineStatusChanged(data); + break; + case "nsPref:changed": + if (data == PREF_APP_UPDATE_LOG) { + gLogEnabled = getPref("getBoolPref", PREF_APP_UPDATE_LOG, false); + } + break; + case "profile-change-net-teardown": // fall thru + case "xpcom-shutdown": + Services.obs.removeObserver(this, topic); + Services.prefs.removeObserver(PREF_APP_UPDATE_LOG, this); + + if (AppConstants.platform == "win" && gUpdateMutexHandle) { + // If we hold the update mutex, let it go! + // The OS would clean this up sometime after shutdown, + // but that would have no guarantee on timing. + closeHandle(gUpdateMutexHandle); + } + if (this._retryTimer) { + this._retryTimer.cancel(); + } + + this.pauseDownload(); + // Prevent leaking the downloader (bug 454964) + this._downloader = null; + break; + } + }, + + /** + * The following needs to happen during the post-update-processing + * notification from nsUpdateServiceStub.js: + * 1. post update processing + * 2. resume of a download that was in progress during a previous session + * 3. start of a complete update download after the failure to apply a partial + * update + */ + + /** + * Perform post-processing on updates lingering in the updates directory + * from a previous application session - either report install failures (and + * optionally attempt to fetch a different version if appropriate) or + * notify the user of install success. + */ + _postUpdateProcessing: function AUS__postUpdateProcessing() { + if (!this.canCheckForUpdates) { + LOG("UpdateService:_postUpdateProcessing - unable to check for " + + "updates... returning early"); + return; + } + + if (!this.canApplyUpdates) { + LOG("UpdateService:_postUpdateProcessing - unable to apply " + + "updates... returning early"); + // If the update is present in the update directly somehow, + // it would prevent us from notifying the user of futher updates. + cleanupActiveUpdate(); + return; + } + + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + var update = um.activeUpdate; + var status = readStatusFile(getUpdatesDir()); + pingStateAndStatusCodes(update, true, status); + // STATE_NONE status typically means that the update.status file is present + // but a background download error occurred. + if (status == STATE_NONE) { + LOG("UpdateService:_postUpdateProcessing - no status, no update"); + cleanupActiveUpdate(); + return; + } + + // Handle the case when the update is the same or older than the current + // version and nsUpdateDriver.cpp skipped updating due to the version being + // older than the current version. + if (update && update.appVersion && + (status == STATE_PENDING || status == STATE_PENDING_SERVICE || + status == STATE_APPLIED || status == STATE_APPLIED_SERVICE || + status == STATE_PENDING_ELEVATE)) { + if (Services.vc.compare(update.appVersion, Services.appinfo.version) < 0 || + Services.vc.compare(update.appVersion, Services.appinfo.version) == 0 && + update.buildID == Services.appinfo.appBuildID) { + LOG("UpdateService:_postUpdateProcessing - removing update for older " + + "or same application version"); + cleanupActiveUpdate(); + return; + } + } + + if (AppConstants.platform == "gonk") { + // This code is called very early in the boot process, before we've even + // had a chance to setup the UI so we can give feedback to the user. + // + // Since the download may be occuring over a link which has associated + // cost, we want to require user-consent before resuming the download. + // Also, applying an already downloaded update now is undesireable, + // since the phone will look dead while the update is being applied. + // Applying the update can take several minutes. Instead we wait until + // the UI is initialized so it is possible to give feedback to and get + // consent to update from the user. + if (isInterruptedUpdate(status)) { + LOG("UpdateService:_postUpdateProcessing - interrupted update detected - wait for user consent"); + return; + } + } + + if (status == STATE_DOWNLOADING) { + LOG("UpdateService:_postUpdateProcessing - patch found in downloading " + + "state"); + if (update && update.state != STATE_SUCCEEDED) { + // Resume download + status = this.downloadUpdate(update, true); + if (status == STATE_NONE) + cleanupActiveUpdate(); + } + return; + } + + if (status == STATE_APPLYING) { + // This indicates that the background updater service is in either of the + // following two states: + // 1. It is in the process of applying an update in the background, and + // we just happen to be racing against that. + // 2. It has failed to apply an update for some reason, and we hit this + // case because the updater process has set the update status to + // applying, but has never finished. + // In order to differentiate between these two states, we look at the + // state field of the update object. If it's "pending", then we know + // that this is the first time we're hitting this case, so we switch + // that state to "applying" and we just wait and hope for the best. + // If it's "applying", we know that we've already been here once, so + // we really want to start from a clean state. + if (update && + (update.state == STATE_PENDING || + update.state == STATE_PENDING_SERVICE)) { + LOG("UpdateService:_postUpdateProcessing - patch found in applying " + + "state for the first time"); + update.state = STATE_APPLYING; + um.saveUpdates(); + } else { // We get here even if we don't have an update object + LOG("UpdateService:_postUpdateProcessing - patch found in applying " + + "state for the second time"); + cleanupActiveUpdate(); + } + return; + } + + if (AppConstants.platform == "gonk") { + // The update is only applied but not selected to be installed + if (status == STATE_APPLIED && update && update.isOSUpdate) { + LOG("UpdateService:_postUpdateProcessing - update staged as applied found"); + return; + } + + if (status == STATE_APPLIED_OS && update && update.isOSUpdate) { + // In gonk, we need to check for OS update status after startup, since + // the recovery partition won't write to update.status for us + let recoveryService = Cc["@mozilla.org/recovery-service;1"]. + getService(Ci.nsIRecoveryService); + let fotaStatus = recoveryService.getFotaUpdateStatus(); + switch (fotaStatus) { + case Ci.nsIRecoveryService.FOTA_UPDATE_SUCCESS: + status = STATE_SUCCEEDED; + break; + case Ci.nsIRecoveryService.FOTA_UPDATE_FAIL: + status = STATE_FAILED + ": " + FOTA_GENERAL_ERROR; + break; + case Ci.nsIRecoveryService.FOTA_UPDATE_UNKNOWN: + default: + status = STATE_FAILED + ": " + FOTA_UNKNOWN_ERROR; + break; + } + } + } + + if (!update) { + if (status != STATE_SUCCEEDED) { + LOG("UpdateService:_postUpdateProcessing - previous patch failed " + + "and no patch available"); + cleanupActiveUpdate(); + return; + } + update = new Update(null); + } + + let parts = status.split(":"); + update.state = parts[0]; + if (update.state == STATE_FAILED && parts[1]) { + update.errorCode = parseInt(parts[1]); + } + + + if (status != STATE_SUCCEEDED) { + // Since the update didn't succeed save a copy of the active update's + // current state to the updates.xml so it is possible to track failures. + um.saveUpdates(); + // Rotate the update logs so the update log isn't removed. By passing + // false the patch directory won't be removed. + cleanUpUpdatesDir(false); + } + + if (status == STATE_SUCCEEDED) { + update.statusText = gUpdateBundle.GetStringFromName("installSuccess"); + + // Update the patch's metadata. + um.activeUpdate = update; + + // Done with this update. Clean it up. + cleanupActiveUpdate(); + } else if (status == STATE_PENDING_ELEVATE) { + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateElevationRequired(); + return; + } else { + // If there was an I/O error it is assumed that the patch is not invalid + // and it is set to pending so an attempt to apply it again will happen + // when the application is restarted. + if (update.state == STATE_FAILED && update.errorCode) { + if (handleUpdateFailure(update, update.errorCode)) { + return; + } + } + + // Something went wrong with the patch application process. + handleFallbackToCompleteUpdate(update, false); + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(update); + } + }, + + /** + * Register an observer when the network comes online, so we can short-circuit + * the app.update.interval when there isn't connectivity + */ + _registerOnlineObserver: function AUS__registerOnlineObserver() { + if (this._registeredOnlineObserver) { + LOG("UpdateService:_registerOnlineObserver - observer already registered"); + return; + } + + LOG("UpdateService:_registerOnlineObserver - waiting for the network to " + + "be online, then forcing another check"); + + Services.obs.addObserver(this, "network:offline-status-changed", false); + this._registeredOnlineObserver = true; + }, + + /** + * Called from the network:offline-status-changed observer. + */ + _offlineStatusChanged: function AUS__offlineStatusChanged(status) { + if (status !== "online") { + return; + } + + Services.obs.removeObserver(this, "network:offline-status-changed"); + this._registeredOnlineObserver = false; + + LOG("UpdateService:_offlineStatusChanged - network is online, forcing " + + "another background check"); + + // the background checker is contained in notify + this._attemptResume(); + }, + + onCheckComplete: function AUS_onCheckComplete(request, updates, updateCount) { + this._selectAndInstallUpdate(updates); + }, + + onError: function AUS_onError(request, update) { + LOG("UpdateService:onError - error during background update. error code: " + + update.errorCode + ", status text: " + update.statusText); + + if (update.errorCode == NETWORK_ERROR_OFFLINE) { + // Register an online observer to try again + this._registerOnlineObserver(); + if (this._pingSuffix) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE); + } + return; + } + + // Send the error code to telemetry + AUSTLMY.pingCheckExError(this._pingSuffix, update.errorCode); + update.errorCode = BACKGROUNDCHECK_MULTIPLE_FAILURES; + let errCount = getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDERRORS, 0); + errCount++; + Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount); + // Don't allow the preference to set a value greater than 20 for max errors. + let maxErrors = Math.min(getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, 10), 20); + + if (errCount >= maxErrors) { + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(update); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT); + } else { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT); + } + }, + + /** + * Called when a connection should be resumed + */ + _attemptResume: function AUS_attemptResume() { + LOG("UpdateService:_attemptResume"); + // If a download is in progress, then resume it. + if (this._downloader && this._downloader._patch && + this._downloader._patch.state == STATE_DOWNLOADING && + this._downloader._update) { + LOG("UpdateService:_attemptResume - _patch.state: " + + this._downloader._patch.state); + // Make sure downloading is the state for selectPatch to work correctly + writeStatusFile(getUpdatesDir(), STATE_DOWNLOADING); + var status = this.downloadUpdate(this._downloader._update, + this._downloader.background); + LOG("UpdateService:_attemptResume - downloadUpdate status: " + status); + if (status == STATE_NONE) { + cleanupActiveUpdate(); + } + return; + } + + this.backgroundChecker.checkForUpdates(this, false); + }, + + /** + * Notified when a timer fires + * @param timer + * The timer that fired + */ + notify: function AUS_notify(timer) { + this._checkForBackgroundUpdates(true); + }, + + /** + * See nsIUpdateService.idl + */ + checkForBackgroundUpdates: function AUS_checkForBackgroundUpdates() { + this._checkForBackgroundUpdates(false); + }, + + // The suffix used for background update check telemetry histogram ID's. + get _pingSuffix() { + return this._isNotify ? AUSTLMY.NOTIFY : AUSTLMY.EXTERNAL; + }, + + /** + * Checks for updates in the background. + * @param isNotify + * Whether or not a background update check was initiated by the + * application update timer notification. + */ + _checkForBackgroundUpdates: function AUS__checkForBackgroundUpdates(isNotify) { + this._isNotify = isNotify; + + // Histogram IDs: + // UPDATE_PING_COUNT_EXTERNAL + // UPDATE_PING_COUNT_NOTIFY + AUSTLMY.pingGeneric("UPDATE_PING_COUNT_" + this._pingSuffix, + true, false); + + // Histogram IDs: + // UPDATE_UNABLE_TO_APPLY_EXTERNAL + // UPDATE_UNABLE_TO_APPLY_NOTIFY + AUSTLMY.pingGeneric("UPDATE_UNABLE_TO_APPLY_" + this._pingSuffix, + getCanApplyUpdates(), true); + // Histogram IDs: + // UPDATE_CANNOT_STAGE_EXTERNAL + // UPDATE_CANNOT_STAGE_NOTIFY + AUSTLMY.pingGeneric("UPDATE_CANNOT_STAGE_" + this._pingSuffix, + getCanStageUpdates(), true); + // Histogram IDs: + // UPDATE_INVALID_LASTUPDATETIME_EXTERNAL + // UPDATE_INVALID_LASTUPDATETIME_NOTIFY + // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL + // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY + AUSTLMY.pingLastUpdateTime(this._pingSuffix); + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_ENABLED_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_ENABLED_NOTIFY + AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_ENABLED_" + this._pingSuffix, + PREF_APP_UPDATE_ENABLED, true, true); + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_AUTO_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_AUTO_NOTIFY + AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_AUTO_" + this._pingSuffix, + PREF_APP_UPDATE_AUTO, true, true); + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_NOTIFY + AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_" + + this._pingSuffix, + PREF_APP_UPDATE_STAGING_ENABLED, true, true); + if (AppConstants.platform == "win" || AppConstants.platform == "macosx") { + // Histogram IDs: + // UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL + // UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY + AUSTLMY.pingIntPref("UPDATE_PREF_UPDATE_CANCELATIONS_" + this._pingSuffix, + PREF_APP_UPDATE_CANCELATIONS, 0, 0); + } + if (AppConstants.platform == "macosx") { + // Histogram IDs: + // UPDATE_PREF_UPDATE_CANCELATIONS_OSX_EXTERNAL + // UPDATE_PREF_UPDATE_CANCELATIONS_OSX_NOTIFY + AUSTLMY.pingIntPref("UPDATE_PREF_UPDATE_CANCELATIONS_OSX_" + + this._pingSuffix, + PREF_APP_UPDATE_CANCELATIONS_OSX, 0, 0); + } + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_NOTIFY + AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_" + + this._pingSuffix, + PREF_APP_UPDATE_SERVICE_ENABLED, true); + // Histogram IDs: + // UPDATE_PREF_SERVICE_ERRORS_EXTERNAL + // UPDATE_PREF_SERVICE_ERRORS_NOTIFY + AUSTLMY.pingIntPref("UPDATE_PREF_SERVICE_ERRORS_" + this._pingSuffix, + PREF_APP_UPDATE_SERVICE_ERRORS, 0, 0); + if (AppConstants.platform == "win") { + // Histogram IDs: + // UPDATE_SERVICE_INSTALLED_EXTERNAL + // UPDATE_SERVICE_INSTALLED_NOTIFY + // UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL + // UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY + AUSTLMY.pingServiceInstallStatus(this._pingSuffix, isServiceInstalled()); + } + } + + // If a download is in progress or the patch has been staged do nothing. + if (this.isDownloading) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADING); + return; + } + + if (this._downloader && this._downloader.patchIsStaged) { + let readState = readStatusFile(getUpdatesDir()); + if (readState == STATE_PENDING || readState == STATE_PENDING_SERVICE || + readState == STATE_PENDING_ELEVATE) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED); + } else { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_STAGED); + } + return; + } + + let validUpdateURL = true; + try { + this.backgroundChecker.getUpdateURL(false); + } catch (e) { + validUpdateURL = false; + } + // The following checks are done here so they can be differentiated from + // foreground checks. + if (!UpdateUtils.OSVersion) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_VERSION); + } else if (!UpdateUtils.ABI) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_ABI); + } else if (!validUpdateURL) { + AUSTLMY.pingCheckCode(this._pingSuffix, + AUSTLMY.CHK_INVALID_DEFAULT_URL); + } else if (!getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true)) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_PREF_DISABLED); + } else if (!hasUpdateMutex()) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_MUTEX); + } else if (!gCanCheckForUpdates) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_CHECK); + } else if (!this.backgroundChecker._enabled) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_FOR_SESSION); + } + + this.backgroundChecker.checkForUpdates(this, false); + }, + + /** + * Determine the update from the specified updates that should be offered. + * If both valid major and minor updates are available the minor update will + * be offered. + * @param updates + * An array of available nsIUpdate items + * @return The nsIUpdate to offer. + */ + selectUpdate: function AUS_selectUpdate(updates) { + if (updates.length == 0) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_UPDATE_FOUND); + return null; + } + + // The ping for unsupported is sent after the call to showPrompt. + if (updates.length == 1 && updates[0].unsupported) { + return updates[0]; + } + + // Choose the newest of the available minor and major updates. + var majorUpdate = null; + var minorUpdate = null; + var vc = Services.vc; + let lastCheckCode = AUSTLMY.CHK_NO_COMPAT_UPDATE_FOUND; + + updates.forEach(function(aUpdate) { + // Ignore updates for older versions of the application and updates for + // the same version of the application with the same build ID. + if (vc.compare(aUpdate.appVersion, Services.appinfo.version) < 0 || + vc.compare(aUpdate.appVersion, Services.appinfo.version) == 0 && + aUpdate.buildID == Services.appinfo.appBuildID) { + LOG("UpdateService:selectUpdate - skipping update because the " + + "update's application version is less than the current " + + "application version"); + lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION; + return; + } + + // Skip the update if the user responded with "never" to this update's + // application version and the update specifies showNeverForVersion + // (see bug 350636). + let neverPrefName = PREFBRANCH_APP_UPDATE_NEVER + aUpdate.appVersion; + if (aUpdate.showNeverForVersion && + getPref("getBoolPref", neverPrefName, false)) { + LOG("UpdateService:selectUpdate - skipping update because the " + + "preference " + neverPrefName + " is true"); + lastCheckCode = AUSTLMY.CHK_UPDATE_NEVER_PREF; + return; + } + + switch (aUpdate.type) { + case "major": + if (!majorUpdate) + majorUpdate = aUpdate; + else if (vc.compare(majorUpdate.appVersion, aUpdate.appVersion) <= 0) + majorUpdate = aUpdate; + break; + case "minor": + if (!minorUpdate) + minorUpdate = aUpdate; + else if (vc.compare(minorUpdate.appVersion, aUpdate.appVersion) <= 0) + minorUpdate = aUpdate; + break; + default: + LOG("UpdateService:selectUpdate - skipping unknown update type: " + + aUpdate.type); + lastCheckCode = AUSTLMY.CHK_UPDATE_INVALID_TYPE; + break; + } + }); + + let update = minorUpdate || majorUpdate; + if (AppConstants.platform == "macosx" && update) { + if (getElevationRequired()) { + let installAttemptVersion = getPref("getCharPref", + PREF_APP_UPDATE_ELEVATE_VERSION, + null); + if (vc.compare(installAttemptVersion, update.appVersion) != 0) { + Services.prefs.setCharPref(PREF_APP_UPDATE_ELEVATE_VERSION, + update.appVersion); + if (Services.prefs.prefHasUserValue( + PREF_APP_UPDATE_CANCELATIONS_OSX)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); + } + } else { + let numCancels = getPref("getIntPref", + PREF_APP_UPDATE_CANCELATIONS_OSX, 0); + let rejectedVersion = getPref("getCharPref", + PREF_APP_UPDATE_ELEVATE_NEVER, ""); + let maxCancels = getPref("getIntPref", + PREF_APP_UPDATE_CANCELATIONS_OSX_MAX, + DEFAULT_CANCELATIONS_OSX_MAX); + if (numCancels >= maxCancels) { + LOG("UpdateService:selectUpdate - the user requires elevation to " + + "install this update, but the user has exceeded the max " + + "number of elevation attempts."); + update.elevationFailure = true; + AUSTLMY.pingCheckCode( + this._pingSuffix, + AUSTLMY.CHK_ELEVATION_DISABLED_FOR_VERSION); + } else if (vc.compare(rejectedVersion, update.appVersion) == 0) { + LOG("UpdateService:selectUpdate - the user requires elevation to " + + "install this update, but elevation is disabled for this " + + "version."); + update.elevationFailure = true; + AUSTLMY.pingCheckCode(this._pingSuffix, + AUSTLMY.CHK_ELEVATION_OPTOUT_FOR_VERSION); + } else { + LOG("UpdateService:selectUpdate - the user requires elevation to " + + "install the update."); + } + } + } else { + // Clear elevation-related prefs since they no longer apply (the user + // may have gained write access to the Firefox directory or an update + // was executed with a different profile). + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_VERSION)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_VERSION); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); + } + } + } else if (!update) { + AUSTLMY.pingCheckCode(this._pingSuffix, lastCheckCode); + } + + return update; + }, + + /** + * Determine which of the specified updates should be installed and begin the + * download/installation process or notify the user about the update. + * @param updates + * An array of available updates + */ + _selectAndInstallUpdate: function AUS__selectAndInstallUpdate(updates) { + // Return early if there's an active update. The user is already aware and + // is downloading or performed some user action to prevent notification. + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + if (um.activeUpdate) { + if (AppConstants.platform == "gonk") { + // For gonk, the user isn't necessarily aware of the update, so we need + // to show the prompt to make sure. + this._showPrompt(um.activeUpdate); + } + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_HAS_ACTIVEUPDATE); + return; + } + + var updateEnabled = getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true); + if (!updateEnabled) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_PREF_DISABLED); + LOG("UpdateService:_selectAndInstallUpdate - not prompting because " + + "update is disabled"); + return; + } + + var update = this.selectUpdate(updates, updates.length); + if (!update || update.elevationFailure) { + return; + } + + if (update.unsupported) { + LOG("UpdateService:_selectAndInstallUpdate - update not supported for " + + "this system"); + if (!getPref("getBoolPref", PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, false)) { + LOG("UpdateService:_selectAndInstallUpdate - notifying that the " + + "update is not supported for this system"); + this._showPrompt(update); + } + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED); + return; + } + + if (!getCanApplyUpdates()) { + LOG("UpdateService:_selectAndInstallUpdate - the user is unable to " + + "apply updates... prompting"); + this._showPrompt(update); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY); + return; + } + + /** + * From this point on there are two possible outcomes: + * 1. download and install the update automatically + * 2. notify the user about the availability of an update + * + * Notes: + * a) if the app.update.auto preference is false then automatic download and + * install is disabled and the user will be notified. + * b) if the update has a showPrompt attribute the user will be notified. + * + * If the update when it is first read does not have an appVersion attribute + * the following deprecated behavior will occur: + * Update Type Outcome + * Major Notify + * Minor Auto Install + */ + if (update.showPrompt) { + LOG("UpdateService:_selectAndInstallUpdate - prompting because the " + + "update snippet specified showPrompt"); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_SNIPPET); + this._showPrompt(update); + return; + } + + if (!getPref("getBoolPref", PREF_APP_UPDATE_AUTO, true)) { + LOG("UpdateService:_selectAndInstallUpdate - prompting because silent " + + "install is disabled"); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF); + this._showPrompt(update); + return; + } + + LOG("UpdateService:_selectAndInstallUpdate - download the update"); + let status = this.downloadUpdate(update, true); + if (status == STATE_NONE) { + cleanupActiveUpdate(); + } + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DOWNLOAD_UPDATE); + }, + + _showPrompt: function AUS__showPrompt(update) { + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateAvailable(update); + }, + + /** + * The Checker used for background update checks. + */ + _backgroundChecker: null, + + /** + * See nsIUpdateService.idl + */ + get backgroundChecker() { + if (!this._backgroundChecker) + this._backgroundChecker = new Checker(); + return this._backgroundChecker; + }, + + /** + * See nsIUpdateService.idl + */ + get canCheckForUpdates() { + return gCanCheckForUpdates && hasUpdateMutex(); + }, + + /** + * See nsIUpdateService.idl + */ + get elevationRequired() { + return getElevationRequired(); + }, + + /** + * See nsIUpdateService.idl + */ + get canApplyUpdates() { + return getCanApplyUpdates() && hasUpdateMutex(); + }, + + /** + * See nsIUpdateService.idl + */ + get canStageUpdates() { + return getCanStageUpdates(); + }, + + /** + * See nsIUpdateService.idl + */ + get isOtherInstanceHandlingUpdates() { + return !hasUpdateMutex(); + }, + + + /** + * See nsIUpdateService.idl + */ + addDownloadListener: function AUS_addDownloadListener(listener) { + if (!this._downloader) { + LOG("UpdateService:addDownloadListener - no downloader!"); + return; + } + this._downloader.addDownloadListener(listener); + }, + + /** + * See nsIUpdateService.idl + */ + removeDownloadListener: function AUS_removeDownloadListener(listener) { + if (!this._downloader) { + LOG("UpdateService:removeDownloadListener - no downloader!"); + return; + } + this._downloader.removeDownloadListener(listener); + }, + + /** + * See nsIUpdateService.idl + */ + downloadUpdate: function AUS_downloadUpdate(update, background) { + if (!update) + throw Cr.NS_ERROR_NULL_POINTER; + + // Don't download the update if the update's version is less than the + // current application's version or the update's version is the same as the + // application's version and the build ID is the same as the application's + // build ID. + if (update.appVersion && + (Services.vc.compare(update.appVersion, Services.appinfo.version) < 0 || + update.buildID && update.buildID == Services.appinfo.appBuildID && + update.appVersion == Services.appinfo.version)) { + LOG("UpdateService:downloadUpdate - canceling download of update since " + + "it is for an earlier or same application version and build ID.\n" + + "current application version: " + Services.appinfo.version + "\n" + + "update application version : " + update.appVersion + "\n" + + "current build ID: " + Services.appinfo.appBuildID + "\n" + + "update build ID : " + update.buildID); + cleanupActiveUpdate(); + return STATE_NONE; + } + + // If a download request is in progress vs. a download ready to resume + if (this.isDownloading) { + if (update.isCompleteUpdate == this._downloader.isCompleteUpdate && + background == this._downloader.background) { + LOG("UpdateService:downloadUpdate - no support for downloading more " + + "than one update at a time"); + return readStatusFile(getUpdatesDir()); + } + this._downloader.cancel(); + } + if (AppConstants.platform == "gonk") { + let um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + let activeUpdate = um.activeUpdate; + if (activeUpdate && + (activeUpdate.appVersion != update.appVersion || + activeUpdate.buildID != update.buildID)) { + // We have an activeUpdate (which presumably was interrupted), and are + // about start downloading a new one. Make sure we remove all traces + // of the active one (otherwise we'll start appending the new update.mar + // the the one that's been partially downloaded). + LOG("UpdateService:downloadUpdate - removing stale active update."); + cleanupActiveUpdate(); + } + } + // Set the previous application version prior to downloading the update. + update.previousAppVersion = Services.appinfo.version; + this._downloader = new Downloader(background, this); + return this._downloader.downloadUpdate(update); + }, + + /** + * See nsIUpdateService.idl + */ + pauseDownload: function AUS_pauseDownload() { + if (this.isDownloading) { + this._downloader.cancel(); + } else if (this._retryTimer) { + // Download status is still consider as 'downloading' during retry. + // We need to cancel both retry and download at this stage. + this._retryTimer.cancel(); + this._retryTimer = null; + this._downloader.cancel(); + } + }, + + /** + * See nsIUpdateService.idl + */ + getUpdatesDirectory: getUpdatesDir, + + /** + * See nsIUpdateService.idl + */ + get isDownloading() { + return this._downloader && this._downloader.isBusy; + }, + + /** + * See nsIUpdateService.idl + */ + applyOsUpdate: function AUS_applyOsUpdate(aUpdate) { + if (!aUpdate.isOSUpdate || aUpdate.state != STATE_APPLIED) { + aUpdate.statusText = "fota-state-error"; + throw Cr.NS_ERROR_FAILURE; + } + + aUpdate.QueryInterface(Ci.nsIWritablePropertyBag); + let osApplyToDir = aUpdate.getProperty("osApplyToDir"); + + if (!osApplyToDir) { + LOG("UpdateService:applyOsUpdate - Error: osApplyToDir is not defined" + + "in the nsIUpdate!"); + pingStateAndStatusCodes(aUpdate, false, + STATE_FAILED + ": " + FOTA_FILE_OPERATION_ERROR); + handleUpdateFailure(aUpdate, FOTA_FILE_OPERATION_ERROR); + return; + } + + let updateFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + updateFile.initWithPath(osApplyToDir + "/update.zip"); + if (!updateFile.exists()) { + LOG("UpdateService:applyOsUpdate - Error: OS update is not found at " + + updateFile.path); + pingStateAndStatusCodes(aUpdate, false, + STATE_FAILED + ": " + FOTA_FILE_OPERATION_ERROR); + handleUpdateFailure(aUpdate, FOTA_FILE_OPERATION_ERROR); + return; + } + + writeStatusFile(getUpdatesDir(), aUpdate.state = STATE_APPLIED_OS); + LOG("UpdateService:applyOsUpdate - Rebooting into recovery to apply " + + "FOTA update: " + updateFile.path); + try { + let recoveryService = Cc["@mozilla.org/recovery-service;1"] + .getService(Ci.nsIRecoveryService); + recoveryService.installFotaUpdate(updateFile.path); + } catch (e) { + LOG("UpdateService:applyOsUpdate - Error: Couldn't reboot into recovery" + + " to apply FOTA update " + updateFile.path); + pingStateAndStatusCodes(aUpdate, false, + STATE_FAILED + ": " + FOTA_RECOVERY_ERROR); + writeStatusFile(getUpdatesDir(), aUpdate.state = STATE_APPLIED); + handleUpdateFailure(aUpdate, FOTA_RECOVERY_ERROR); + } + }, + + classID: UPDATESERVICE_CID, + classInfo: XPCOMUtils.generateCI({classID: UPDATESERVICE_CID, + contractID: UPDATESERVICE_CONTRACTID, + interfaces: [Ci.nsIApplicationUpdateService, + Ci.nsITimerCallback, + Ci.nsIObserver], + flags: Ci.nsIClassInfo.SINGLETON}), + + _xpcom_factory: UpdateServiceFactory, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIApplicationUpdateService, + Ci.nsIUpdateCheckListener, + Ci.nsITimerCallback, + Ci.nsIObserver]) +}; + +/** + * A service to manage active and past updates. + * @constructor + */ +function UpdateManager() { + // Ensure the Active Update file is loaded + var updates = this._loadXMLFileIntoArray(getUpdateFile( + [FILE_ACTIVE_UPDATE_XML])); + if (updates.length > 0) { + // Under some edgecases such as Windows system restore the active-update.xml + // will contain a pending update without the status file which will return + // STATE_NONE. To recover from this situation clean the updates dir and + // rewrite the active-update.xml file without the broken update. + if (readStatusFile(getUpdatesDir()) == STATE_NONE) { + cleanUpUpdatesDir(); + this._writeUpdatesToXMLFile([], getUpdateFile([FILE_ACTIVE_UPDATE_XML])); + } + else + this._activeUpdate = updates[0]; + } +} +UpdateManager.prototype = { + /** + * All previously downloaded and installed updates, as an array of nsIUpdate + * objects. + */ + _updates: null, + + /** + * The current actively downloading/installing update, as a nsIUpdate object. + */ + _activeUpdate: null, + + /** + * Handle Observer Service notifications + * @param subject + * The subject of the notification + * @param topic + * The notification name + * @param data + * Additional data + */ + observe: function UM_observe(subject, topic, data) { + // Hack to be able to run and cleanup tests by reloading the update data. + if (topic == "um-reload-update-data") { + this._updates = this._loadXMLFileIntoArray(getUpdateFile( + [FILE_UPDATES_XML])); + this._activeUpdate = null; + var updates = this._loadXMLFileIntoArray(getUpdateFile( + [FILE_ACTIVE_UPDATE_XML])); + if (updates.length > 0) + this._activeUpdate = updates[0]; + } + }, + + /** + * Loads an updates.xml formatted file into an array of nsIUpdate items. + * @param file + * A nsIFile for the updates.xml file + * @return The array of nsIUpdate items held in the file. + */ + _loadXMLFileIntoArray: function UM__loadXMLFileIntoArray(file) { + if (!file.exists()) { + LOG("UpdateManager:_loadXMLFileIntoArray: XML file does not exist"); + return []; + } + + var result = []; + var fileStream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); + try { + var parser = Cc["@mozilla.org/xmlextras/domparser;1"]. + createInstance(Ci.nsIDOMParser); + var doc = parser.parseFromStream(fileStream, "UTF-8", + fileStream.available(), "text/xml"); + + const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; + var updateCount = doc.documentElement.childNodes.length; + for (var i = 0; i < updateCount; ++i) { + var updateElement = doc.documentElement.childNodes.item(i); + if (updateElement.nodeType != ELEMENT_NODE || + updateElement.localName != "update") + continue; + + updateElement.QueryInterface(Ci.nsIDOMElement); + let update; + try { + update = new Update(updateElement); + } catch (e) { + LOG("UpdateManager:_loadXMLFileIntoArray - invalid update"); + continue; + } + result.push(update); + } + } + catch (e) { + LOG("UpdateManager:_loadXMLFileIntoArray - error constructing update " + + "list. Exception: " + e); + } + fileStream.close(); + return result; + }, + + /** + * Load the update manager, initializing state from state files. + */ + _ensureUpdates: function UM__ensureUpdates() { + if (!this._updates) { + this._updates = this._loadXMLFileIntoArray(getUpdateFile( + [FILE_UPDATES_XML])); + var activeUpdates = this._loadXMLFileIntoArray(getUpdateFile( + [FILE_ACTIVE_UPDATE_XML])); + if (activeUpdates.length > 0) + this._activeUpdate = activeUpdates[0]; + } + }, + + /** + * See nsIUpdateService.idl + */ + getUpdateAt: function UM_getUpdateAt(index) { + this._ensureUpdates(); + return this._updates[index]; + }, + + /** + * See nsIUpdateService.idl + */ + get updateCount() { + this._ensureUpdates(); + return this._updates.length; + }, + + /** + * See nsIUpdateService.idl + */ + get activeUpdate() { + if (this._activeUpdate && + this._activeUpdate.channel != UpdateUtils.UpdateChannel) { + LOG("UpdateManager:get activeUpdate - channel has changed, " + + "reloading default preferences to workaround bug 802022"); + // Workaround to get distribution preferences loaded (Bug 774618). This + // can be removed after bug 802022 is fixed. + let prefSvc = Services.prefs.QueryInterface(Ci.nsIObserver); + prefSvc.observe(null, "reload-default-prefs", null); + if (this._activeUpdate.channel != UpdateUtils.UpdateChannel) { + // User switched channels, clear out any old active updates and remove + // partial downloads + this._activeUpdate = null; + this.saveUpdates(); + + // Destroy the updates directory, since we're done with it. + cleanUpUpdatesDir(); + } + } + return this._activeUpdate; + }, + set activeUpdate(activeUpdate) { + this._addUpdate(activeUpdate); + this._activeUpdate = activeUpdate; + if (!activeUpdate) { + // If |activeUpdate| is null, we have updated both lists - the active list + // and the history list, so we want to write both files. + this.saveUpdates(); + } + else + this._writeUpdatesToXMLFile([this._activeUpdate], + getUpdateFile([FILE_ACTIVE_UPDATE_XML])); + return activeUpdate; + }, + + /** + * Add an update to the Updates list. If the item already exists in the list, + * replace the existing value with the new value. + * @param update + * The nsIUpdate object to add. + */ + _addUpdate: function UM__addUpdate(update) { + if (!update) + return; + this._ensureUpdates(); + if (this._updates) { + for (var i = 0; i < this._updates.length; ++i) { + // Keep all update entries with a state of STATE_FAILED and replace the + // first update entry that has the same application version and build ID + // if it exists. This allows the update history to only have one success + // entry for an update and entries for all failed updates. + if (update.state != STATE_FAILED && + this._updates[i] && + this._updates[i].state != STATE_FAILED && + this._updates[i].appVersion == update.appVersion && + this._updates[i].buildID == update.buildID) { + // Replace the existing entry with the new value, updating + // all metadata. + this._updates[i] = update; + return; + } + } + } + // Otherwise add it to the front of the list. + this._updates.unshift(update); + }, + + /** + * Serializes an array of updates to an XML file + * @param updates + * An array of nsIUpdate objects + * @param file + * The nsIFile object to serialize to + */ + _writeUpdatesToXMLFile: function UM__writeUpdatesToXMLFile(updates, file) { + var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + var modeFlags = FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | + FileUtils.MODE_TRUNCATE; + if (!file.exists()) { + file.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + } + fos.init(file, modeFlags, FileUtils.PERMS_FILE, 0); + + try { + var parser = Cc["@mozilla.org/xmlextras/domparser;1"]. + createInstance(Ci.nsIDOMParser); + const EMPTY_UPDATES_DOCUMENT = "<?xml version=\"1.0\"?><updates xmlns=\"http://www.mozilla.org/2005/app-update\"></updates>"; + var doc = parser.parseFromString(EMPTY_UPDATES_DOCUMENT, "text/xml"); + + for (var i = 0; i < updates.length; ++i) { + // If appVersion isn't defined don't add the update. This happens when + // cleaning up invalid updates (e.g. incorrect channel). + if (updates[i] && updates[i].appVersion) { + doc.documentElement.appendChild(updates[i].serialize(doc)); + } + } + + var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"]. + createInstance(Ci.nsIDOMSerializer); + serializer.serializeToStream(doc.documentElement, fos, null); + } catch (e) { + } + + FileUtils.closeSafeFileOutputStream(fos); + }, + + /** + * See nsIUpdateService.idl + */ + saveUpdates: function UM_saveUpdates() { + this._writeUpdatesToXMLFile([this._activeUpdate], + getUpdateFile([FILE_ACTIVE_UPDATE_XML])); + if (this._activeUpdate) + this._addUpdate(this._activeUpdate); + + this._ensureUpdates(); + // Don't write updates that have a temporary state to the updates.xml file. + if (this._updates) { + let updates = this._updates.slice(); + for (let i = updates.length - 1; i >= 0; --i) { + let state = updates[i].state; + if (state == STATE_NONE || state == STATE_DOWNLOADING || + state == STATE_APPLIED || state == STATE_APPLIED_SERVICE || + state == STATE_PENDING || state == STATE_PENDING_SERVICE || + state == STATE_PENDING_ELEVATE) { + updates.splice(i, 1); + } + } + + this._writeUpdatesToXMLFile(updates.slice(0, 20), + getUpdateFile([FILE_UPDATES_XML])); + } + }, + + /** + * See nsIUpdateService.idl + */ + refreshUpdateStatus: function UM_refreshUpdateStatus() { + var update = this._activeUpdate; + if (!update) { + return; + } + var status = readStatusFile(getUpdatesDir()); + pingStateAndStatusCodes(update, false, status); + var parts = status.split(":"); + update.state = parts[0]; + if (update.state == STATE_FAILED && parts[1]) { + update.errorCode = parseInt(parts[1]); + } + let um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + // Save a copy of the active update's current state to the updates.xml so + // it is possible to track failures. + um.saveUpdates(); + + // Rotate the update logs so the update log isn't removed if a complete + // update is downloaded. By passing false the patch directory won't be + // removed. + cleanUpUpdatesDir(false); + + if (update.state == STATE_FAILED && parts[1]) { + if (!handleUpdateFailure(update, parts[1])) { + handleFallbackToCompleteUpdate(update, true); + } + + update.QueryInterface(Ci.nsIWritablePropertyBag); + update.setProperty("stagingFailed", "true"); + } + if (update.state == STATE_APPLIED && shouldUseService()) { + writeStatusFile(getUpdatesDir(), update.state = STATE_APPLIED_SERVICE); + } + + // Send an observer notification which the update wizard uses in + // order to update its UI. + LOG("UpdateManager:refreshUpdateStatus - Notifying observers that " + + "the update was staged. state: " + update.state + ", status: " + status); + Services.obs.notifyObservers(null, "update-staged", update.state); + + if (AppConstants.platform == "gonk") { + // Do this after everything else, since it will likely cause the app to + // shut down. + if (update.state == STATE_APPLIED) { + // Notify the user that an update has been staged and is ready for + // installation (i.e. that they should restart the application). We do + // not notify on failed update attempts. + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateDownloaded(update, true); + } else { + releaseSDCardMountLock(); + } + return; + } + // Only prompt when the UI isn't already open. + let windowType = getPref("getCharPref", PREF_APP_UPDATE_ALTWINDOWTYPE, null); + if (Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME) || + windowType && Services.wm.getMostRecentWindow(windowType)) { + return; + } + + if (update.state == STATE_APPLIED || + update.state == STATE_APPLIED_SERVICE || + update.state == STATE_PENDING || + update.state == STATE_PENDING_SERVICE || + update.state == STATE_PENDING_ELEVATE) { + // Notify the user that an update has been staged and is ready for + // installation (i.e. that they should restart the application). + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateDownloaded(update, true); + } + }, + + /** + * See nsIUpdateService.idl + */ + elevationOptedIn: function UM_elevationOptedIn() { + // The user has been been made aware that the update requires elevation. + let update = this._activeUpdate; + if (!update) { + return; + } + let status = readStatusFile(getUpdatesDir()); + let parts = status.split(":"); + update.state = parts[0]; + if (update.state == STATE_PENDING_ELEVATE) { + // Proceed with the pending update. + // Note: STATE_PENDING_ELEVATE stands for "pending user's approval to + // proceed with an elevated update". As long as we see this state, we will + // notify the user of the availability of an update that requires + // elevation. |elevationOptedIn| (this function) is called when the user + // gives us approval to proceed, so we want to switch to STATE_PENDING. + // The updater then detects whether or not elevation is required and + // displays the elevation prompt if necessary. This last step does not + // depend on the state in the status file. + writeStatusFile(getUpdatesDir(), STATE_PENDING); + } + }, + + /** + * See nsIUpdateService.idl + */ + cleanupActiveUpdate: function UM_cleanupActiveUpdate() { + cleanupActiveUpdate(); + }, + + classID: Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateManager, Ci.nsIObserver]) +}; + +/** + * Checker + * Checks for new Updates + * @constructor + */ +function Checker() { +} +Checker.prototype = { + /** + * The XMLHttpRequest object that performs the connection. + */ + _request: null, + + /** + * The nsIUpdateCheckListener callback + */ + _callback: null, + + /** + * The URL of the update service XML file to connect to that contains details + * about available updates. + */ + getUpdateURL: function UC_getUpdateURL(force) { + this._forced = force; + + let url; + try { + url = Services.prefs.getDefaultBranch(null). + getCharPref(PREF_APP_UPDATE_URL); + } catch (e) { + } + + if (!url || url == "") { + LOG("Checker:getUpdateURL - update URL not defined"); + return null; + } + + url = UpdateUtils.formatUpdateURL(url); + + if (force) { + url += (url.indexOf("?") != -1 ? "&" : "?") + "force=1"; + } + + LOG("Checker:getUpdateURL - update URL: " + url); + return url; + }, + + /** + * See nsIUpdateService.idl + */ + checkForUpdates: function UC_checkForUpdates(listener, force) { + LOG("Checker: checkForUpdates, force: " + force); + if (!listener) + throw Cr.NS_ERROR_NULL_POINTER; + + Services.obs.notifyObservers(null, "update-check-start", null); + + var url = this.getUpdateURL(force); + if (!url || (!this.enabled && !force)) + return; + + this._request = new XMLHttpRequest(); + this._request.open("GET", url, true); + this._request.channel.notificationCallbacks = new gCertUtils.BadCertHandler(false); + // Prevent the request from reading from the cache. + this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + // Prevent the request from writing to the cache. + this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + // Disable cutting edge features, like TLS 1.3, where middleboxes might brick us + this._request.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true; + + this._request.overrideMimeType("text/xml"); + // The Cache-Control header is only interpreted by proxies and the + // final destination. It does not help if a resource is already + // cached locally. + this._request.setRequestHeader("Cache-Control", "no-cache"); + // HTTP/1.0 servers might not implement Cache-Control and + // might only implement Pragma: no-cache + this._request.setRequestHeader("Pragma", "no-cache"); + + var self = this; + this._request.addEventListener("error", function(event) { self.onError(event); }, false); + this._request.addEventListener("load", function(event) { self.onLoad(event); }, false); + + LOG("Checker:checkForUpdates - sending request to: " + url); + this._request.send(null); + + this._callback = listener; + }, + + /** + * Returns an array of nsIUpdate objects discovered by the update check. + * @throws if the XML document element node name is not updates. + */ + get _updates() { + var updatesElement = this._request.responseXML.documentElement; + if (!updatesElement) { + LOG("Checker:_updates get - empty updates document?!"); + return []; + } + + if (updatesElement.nodeName != "updates") { + LOG("Checker:_updates get - unexpected node name!"); + throw new Error("Unexpected node name, expected: updates, got: " + + updatesElement.nodeName); + } + + const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; + var updates = []; + for (var i = 0; i < updatesElement.childNodes.length; ++i) { + var updateElement = updatesElement.childNodes.item(i); + if (updateElement.nodeType != ELEMENT_NODE || + updateElement.localName != "update") + continue; + + updateElement.QueryInterface(Ci.nsIDOMElement); + let update; + try { + update = new Update(updateElement); + } catch (e) { + LOG("Checker:_updates get - invalid <update/>, ignoring..."); + continue; + } + update.serviceURL = this.getUpdateURL(this._forced); + update.channel = UpdateUtils.UpdateChannel; + updates.push(update); + } + + return updates; + }, + + /** + * Returns the status code for the XMLHttpRequest + */ + _getChannelStatus: function UC__getChannelStatus(request) { + var status = 0; + try { + status = request.status; + } + catch (e) { + } + + if (status == 0) + status = request.channel.QueryInterface(Ci.nsIRequest).status; + return status; + }, + + _isHttpStatusCode: function UC__isHttpStatusCode(status) { + return status >= 100 && status <= 599; + }, + + /** + * The XMLHttpRequest succeeded and the document was loaded. + * @param event + * The nsIDOMEvent for the load + */ + onLoad: function UC_onLoad(event) { + LOG("Checker:onLoad - request completed downloading document"); + + try { + // Analyze the resulting DOM and determine the set of updates. + var updates = this._updates; + LOG("Checker:onLoad - number of updates available: " + updates.length); + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS); + } + + // Tell the callback about the updates + this._callback.onCheckComplete(event.target, updates, updates.length); + } catch (e) { + LOG("Checker:onLoad - there was a problem checking for updates. " + + "Exception: " + e); + var request = event.target; + var status = this._getChannelStatus(request); + LOG("Checker:onLoad - request.status: " + status); + var update = new Update(null); + update.errorCode = status; + update.statusText = getStatusTextFromCode(status, 404); + + if (this._isHttpStatusCode(status)) { + update.errorCode = HTTP_ERROR_OFFSET + status; + } + + this._callback.onError(request, update); + } + + this._callback = null; + this._request = null; + }, + + /** + * There was an error of some kind during the XMLHttpRequest + * @param event + * The nsIDOMEvent for the error + */ + onError: function UC_onError(event) { + var request = event.target; + var status = this._getChannelStatus(request); + LOG("Checker:onError - request.status: " + status); + + // If we can't find an error string specific to this status code, + // just use the 200 message from above, which means everything + // "looks" fine but there was probably an XML error or a bogus file. + var update = new Update(null); + update.errorCode = status; + update.statusText = getStatusTextFromCode(status, 200); + + if (status == Cr.NS_ERROR_OFFLINE) { + // We use a separate constant here because nsIUpdate.errorCode is signed + update.errorCode = NETWORK_ERROR_OFFLINE; + } else if (this._isHttpStatusCode(status)) { + update.errorCode = HTTP_ERROR_OFFSET + status; + } + + this._callback.onError(request, update); + this._request = null; + }, + + /** + * Whether or not we are allowed to do update checking. + */ + _enabled: true, + get enabled() { + return getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true) && + gCanCheckForUpdates && hasUpdateMutex() && this._enabled; + }, + + /** + * See nsIUpdateService.idl + */ + stopChecking: function UC_stopChecking(duration) { + // Always stop the current check + if (this._request) + this._request.abort(); + + switch (duration) { + case Ci.nsIUpdateChecker.CURRENT_SESSION: + this._enabled = false; + break; + case Ci.nsIUpdateChecker.ANY_CHECKS: + this._enabled = false; + Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, this._enabled); + break; + } + + this._callback = null; + }, + + classID: Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateChecker]) +}; + +/** + * Manages the download of updates + * @param background + * Whether or not this downloader is operating in background + * update mode. + * @param updateService + * The update service that created this downloader. + * @constructor + */ +function Downloader(background, updateService) { + LOG("Creating Downloader"); + this.background = background; + this.updateService = updateService; +} +Downloader.prototype = { + /** + * The nsIUpdatePatch that we are downloading + */ + _patch: null, + + /** + * The nsIUpdate that we are downloading + */ + _update: null, + + /** + * The nsIIncrementalDownload object handling the download + */ + _request: null, + + /** + * Whether or not the update being downloaded is a complete replacement of + * the user's existing installation or a patch representing the difference + * between the new version and the previous version. + */ + isCompleteUpdate: null, + + /** + * Cancels the active download. + */ + cancel: function Downloader_cancel(cancelError) { + LOG("Downloader: cancel"); + if (cancelError === undefined) { + cancelError = Cr.NS_BINDING_ABORTED; + } + if (this._request && this._request instanceof Ci.nsIRequest) { + this._request.cancel(cancelError); + } + if (AppConstants.platform == "gonk") { + releaseSDCardMountLock(); + } + }, + + /** + * Whether or not a patch has been downloaded and staged for installation. + */ + get patchIsStaged() { + var readState = readStatusFile(getUpdatesDir()); + // Note that if we decide to download and apply new updates after another + // update has been successfully applied in the background, we need to stop + // checking for the APPLIED state here. + return readState == STATE_PENDING || readState == STATE_PENDING_SERVICE || + readState == STATE_PENDING_ELEVATE || + readState == STATE_APPLIED || readState == STATE_APPLIED_SERVICE; + }, + + /** + * Verify the downloaded file. We assume that the download is complete at + * this point. + */ + _verifyDownload: function Downloader__verifyDownload() { + LOG("Downloader:_verifyDownload called"); + if (!this._request) { + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_VERIFY_NO_REQUEST); + return false; + } + + let destination = this._request.destination; + + // Ensure that the file size matches the expected file size. + if (destination.fileSize != this._patch.size) { + LOG("Downloader:_verifyDownload downloaded size != expected size."); + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_VERIFY_PATCH_SIZE_NOT_EQUAL); + return false; + } + + LOG("Downloader:_verifyDownload downloaded size == expected size."); + + // The hash check is not necessary when mar signatures are used to verify + // the downloaded mar file. + if (AppConstants.MOZ_VERIFY_MAR_SIGNATURE) { + return true; + } + + let fileStream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fileStream.init(destination, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); + + let digest; + try { + let hash = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + var hashFunction = Ci.nsICryptoHash[this._patch.hashFunction.toUpperCase()]; + if (hashFunction == undefined) { + throw Cr.NS_ERROR_UNEXPECTED; + } + hash.init(hashFunction); + hash.updateFromStream(fileStream, -1); + // NOTE: For now, we assume that the format of _patch.hashValue is hex + // encoded binary (such as what is typically output by programs like + // sha1sum). In the future, this may change to base64 depending on how + // we choose to compute these hashes. + digest = binaryToHex(hash.finish(false)); + } catch (e) { + LOG("Downloader:_verifyDownload - failed to compute hash of the " + + "downloaded update archive"); + digest = ""; + } + + fileStream.close(); + + if (digest == this._patch.hashValue.toLowerCase()) { + LOG("Downloader:_verifyDownload hashes match."); + return true; + } + + LOG("Downloader:_verifyDownload hashes do not match. "); + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_VERIFY_NO_HASH_MATCH); + return false; + }, + + /** + * Select the patch to use given the current state of updateDir and the given + * set of update patches. + * @param update + * A nsIUpdate object to select a patch from + * @param updateDir + * A nsIFile representing the update directory + * @return A nsIUpdatePatch object to download + */ + _selectPatch: function Downloader__selectPatch(update, updateDir) { + // Given an update to download, we will always try to download the patch + // for a partial update over the patch for a full update. + + /** + * Return the first UpdatePatch with the given type. + * @param type + * The type of the patch ("complete" or "partial") + * @return A nsIUpdatePatch object matching the type specified + */ + function getPatchOfType(type) { + for (var i = 0; i < update.patchCount; ++i) { + var patch = update.getPatchAt(i); + if (patch && patch.type == type) + return patch; + } + return null; + } + + // Look to see if any of the patches in the Update object has been + // pre-selected for download, otherwise we must figure out which one + // to select ourselves. + var selectedPatch = update.selectedPatch; + + var state = readStatusFile(updateDir); + + // If this is a patch that we know about, then select it. If it is a patch + // that we do not know about, then remove it and use our default logic. + var useComplete = false; + if (selectedPatch) { + LOG("Downloader:_selectPatch - found existing patch with state: " + + state); + if (state == STATE_DOWNLOADING) { + LOG("Downloader:_selectPatch - resuming download"); + return selectedPatch; + } + + if (AppConstants.platform == "gonk") { + if (state == STATE_PENDING || state == STATE_APPLYING) { + LOG("Downloader:_selectPatch - resuming interrupted apply"); + return selectedPatch; + } + if (state == STATE_APPLIED) { + LOG("Downloader:_selectPatch - already downloaded and staged"); + return null; + } + } else if (state == STATE_PENDING || state == STATE_PENDING_SERVICE || + state == STATE_PENDING_ELEVATE) { + LOG("Downloader:_selectPatch - already downloaded and staged"); + return null; + } + + if (update && selectedPatch.type == "complete") { + // This is a pretty fatal error. Just bail. + LOG("Downloader:_selectPatch - failed to apply complete patch!"); + writeStatusFile(updateDir, STATE_NONE); + writeVersionFile(getUpdatesDir(), null); + return null; + } + + // Something went wrong when we tried to apply the previous patch. + // Try the complete patch next time. + useComplete = true; + selectedPatch = null; + } + + // If we were not able to discover an update from a previous download, we + // select the best patch from the given set. + var partialPatch = getPatchOfType("partial"); + if (!useComplete) + selectedPatch = partialPatch; + if (!selectedPatch) { + if (partialPatch) + partialPatch.selected = false; + selectedPatch = getPatchOfType("complete"); + } + + // Restore the updateDir since we may have deleted it. + updateDir = getUpdatesDir(); + + // if update only contains a partial patch, selectedPatch == null here if + // the partial patch has been attempted and fails and we're trying to get a + // complete patch + if (selectedPatch) + selectedPatch.selected = true; + + update.isCompleteUpdate = useComplete; + + // Reset the Active Update object on the Update Manager and flush the + // Active Update DB. + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + um.activeUpdate = update; + + return selectedPatch; + }, + + /** + * Whether or not we are currently downloading something. + */ + get isBusy() { + return this._request != null; + }, + + /** + * Get the nsIFile to use for downloading the active update's selected patch + */ + _getUpdateArchiveFile: function Downloader__getUpdateArchiveFile() { + var updateArchive; + if (AppConstants.platform == "gonk") { + try { + updateArchive = FileUtils.getDir(KEY_UPDATE_ARCHIVE_DIR, [], true); + } catch (e) { + return null; + } + } else { + updateArchive = getUpdatesDir().clone(); + } + + updateArchive.append(FILE_UPDATE_MAR); + return updateArchive; + }, + + /** + * Download and stage the given update. + * @param update + * A nsIUpdate object to download a patch for. Cannot be null. + */ + downloadUpdate: function Downloader_downloadUpdate(update) { + LOG("UpdateService:_downloadUpdate"); + if (!update) { + AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE); + throw Cr.NS_ERROR_NULL_POINTER; + } + + var updateDir = getUpdatesDir(); + + this._update = update; + + // This function may return null, which indicates that there are no patches + // to download. + this._patch = this._selectPatch(update, updateDir); + if (!this._patch) { + LOG("Downloader:downloadUpdate - no patch to download"); + AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE_PATCH); + return readStatusFile(updateDir); + } + this.isCompleteUpdate = this._patch.type == "complete"; + + let patchFile = null; + + // Only used by gonk + let status = STATE_NONE; + if (AppConstants.platform == "gonk") { + status = readStatusFile(updateDir); + if (isInterruptedUpdate(status)) { + LOG("Downloader:downloadUpdate - interruptted update"); + // The update was interrupted. Try to locate the existing patch file. + // For an interrupted download, this allows a resume rather than a + // re-download. + patchFile = getFileFromUpdateLink(updateDir); + if (!patchFile) { + // No link file. We'll just assume that the update.mar is in the + // update directory. + patchFile = updateDir.clone(); + patchFile.append(FILE_UPDATE_MAR); + } + if (patchFile.exists()) { + LOG("Downloader:downloadUpdate - resuming with patchFile " + patchFile.path); + if (patchFile.fileSize == this._patch.size) { + LOG("Downloader:downloadUpdate - patchFile appears to be fully downloaded"); + // Bump the status along so that we don't try to redownload again. + if (getElevationRequired()) { + status = STATE_PENDING_ELEVATE; + } else { + status = STATE_PENDING; + } + } + } else { + LOG("Downloader:downloadUpdate - patchFile " + patchFile.path + + " doesn't exist - performing full download"); + // The patchfile doesn't exist, we might as well treat this like + // a new download. + patchFile = null; + } + if (patchFile && status != STATE_DOWNLOADING) { + // It looks like the patch was downloaded, but got interrupted while it + // was being verified or applied. So we'll fake the downloading portion. + + if (getElevationRequired()) { + writeStatusFile(updateDir, STATE_PENDING_ELEVATE); + } else { + writeStatusFile(updateDir, STATE_PENDING); + } + + // Since the code expects the onStopRequest callback to happen + // asynchronously (And you have to call AUS_addDownloadListener + // after calling AUS_downloadUpdate) we need to defer this. + + this._downloadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._downloadTimer.initWithCallback(function() { + this._downloadTimer = null; + // Send a fake onStopRequest. Filling in the destination allows + // _verifyDownload to work, and then the update will be applied. + this._request = {destination: patchFile}; + this.onStopRequest(this._request, null, Cr.NS_OK); + }.bind(this), 0, Ci.nsITimer.TYPE_ONE_SHOT); + + // Returning STATE_DOWNLOADING makes UpdatePrompt think we're + // downloading. The onStopRequest that we spoofed above will make it + // look like the download finished. + return STATE_DOWNLOADING; + } + } + } + + if (!patchFile) { + // Find a place to put the patchfile that we're going to download. + patchFile = this._getUpdateArchiveFile(); + } + if (!patchFile) { + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_NO_PATCH_FILE); + return STATE_NONE; + } + + if (AppConstants.platform == "gonk") { + if (patchFile.path.indexOf(updateDir.path) != 0) { + // The patchFile is in a directory which is different from the + // updateDir, create a link file. + writeLinkFile(updateDir, patchFile); + + if (!isInterruptedUpdate(status) && patchFile.exists()) { + // Remove stale patchFile + patchFile.remove(false); + } + } + } + + update.QueryInterface(Ci.nsIPropertyBag); + let interval = this.background ? update.getProperty("backgroundInterval") + : DOWNLOAD_FOREGROUND_INTERVAL; + + var uri = Services.io.newURI(this._patch.URL, null, null); + LOG("Downloader:downloadUpdate - url: " + uri.spec + ", path: " + + patchFile.path + ", interval: " + interval); + + this._request = Cc["@mozilla.org/network/incremental-download;1"]. + createInstance(Ci.nsIIncrementalDownload); + this._request.init(uri, patchFile, DOWNLOAD_CHUNK_SIZE, interval); + this._request.start(this, null); + + writeStatusFile(updateDir, STATE_DOWNLOADING); + this._patch.QueryInterface(Ci.nsIWritablePropertyBag); + this._patch.state = STATE_DOWNLOADING; + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + um.saveUpdates(); + return STATE_DOWNLOADING; + }, + + /** + * An array of download listeners to notify when we receive + * nsIRequestObserver or nsIProgressEventSink method calls. + */ + _listeners: [], + + /** + * Adds a listener to the download process + * @param listener + * A download listener, implementing nsIRequestObserver and + * nsIProgressEventSink + */ + addDownloadListener: function Downloader_addDownloadListener(listener) { + for (var i = 0; i < this._listeners.length; ++i) { + if (this._listeners[i] == listener) + return; + } + this._listeners.push(listener); + }, + + /** + * Removes a download listener + * @param listener + * The listener to remove. + */ + removeDownloadListener: function Downloader_removeDownloadListener(listener) { + for (var i = 0; i < this._listeners.length; ++i) { + if (this._listeners[i] == listener) { + this._listeners.splice(i, 1); + return; + } + } + }, + + /** + * When the async request begins + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + */ + onStartRequest: function Downloader_onStartRequest(request, context) { + if (request instanceof Ci.nsIIncrementalDownload) + LOG("Downloader:onStartRequest - original URI spec: " + request.URI.spec + + ", final URI spec: " + request.finalURI.spec); + // Always set finalURL in onStartRequest since it can change. + this._patch.finalURL = request.finalURI.spec; + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + um.saveUpdates(); + + var listeners = this._listeners.concat(); + var listenerCount = listeners.length; + for (var i = 0; i < listenerCount; ++i) + listeners[i].onStartRequest(request, context); + }, + + /** + * When new data has been downloaded + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + * @param progress + * The current number of bytes transferred + * @param maxProgress + * The total number of bytes that must be transferred + */ + onProgress: function Downloader_onProgress(request, context, progress, + maxProgress) { + LOG("Downloader:onProgress - progress: " + progress + "/" + maxProgress); + + if (progress > this._patch.size) { + LOG("Downloader:onProgress - progress: " + progress + + " is higher than patch size: " + this._patch.size); + // It's important that we use a different code than + // NS_ERROR_CORRUPTED_CONTENT so that tests can verify the difference + // between a hash error and a wrong download error. + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_PATCH_SIZE_LARGER); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + return; + } + + if (maxProgress != this._patch.size) { + LOG("Downloader:onProgress - maxProgress: " + maxProgress + + " is not equal to expected patch size: " + this._patch.size); + // It's important that we use a different code than + // NS_ERROR_CORRUPTED_CONTENT so that tests can verify the difference + // between a hash error and a wrong download error. + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_PATCH_SIZE_NOT_EQUAL); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + return; + } + + var listeners = this._listeners.concat(); + var listenerCount = listeners.length; + for (var i = 0; i < listenerCount; ++i) { + var listener = listeners[i]; + if (listener instanceof Ci.nsIProgressEventSink) + listener.onProgress(request, context, progress, maxProgress); + } + this.updateService._consecutiveSocketErrors = 0; + }, + + /** + * When we have new status text + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + * @param status + * A status code + * @param statusText + * Human readable version of |status| + */ + onStatus: function Downloader_onStatus(request, context, status, statusText) { + LOG("Downloader:onStatus - status: " + status + ", statusText: " + + statusText); + + var listeners = this._listeners.concat(); + var listenerCount = listeners.length; + for (var i = 0; i < listenerCount; ++i) { + var listener = listeners[i]; + if (listener instanceof Ci.nsIProgressEventSink) + listener.onStatus(request, context, status, statusText); + } + }, + + /** + * When data transfer ceases + * @param request + * The nsIRequest object for the transfer + * @param context + * Additional data + * @param status + * Status code containing the reason for the cessation. + */ + onStopRequest: function Downloader_onStopRequest(request, context, status) { + if (request instanceof Ci.nsIIncrementalDownload) + LOG("Downloader:onStopRequest - original URI spec: " + request.URI.spec + + ", final URI spec: " + request.finalURI.spec + ", status: " + status); + + // XXX ehsan shouldShowPrompt should always be false here. + // But what happens when there is already a UI showing? + var state = this._patch.state; + var shouldShowPrompt = false; + var shouldRegisterOnlineObserver = false; + var shouldRetrySoon = false; + var deleteActiveUpdate = false; + var retryTimeout = getPref("getIntPref", PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT, + DEFAULT_SOCKET_RETRYTIMEOUT); + // Prevent the preference from setting a value greater than 10000. + retryTimeout = Math.min(retryTimeout, 10000); + var maxFail = getPref("getIntPref", PREF_APP_UPDATE_SOCKET_MAXERRORS, + DEFAULT_SOCKET_MAX_ERRORS); + // Prevent the preference from setting a value greater than 20. + maxFail = Math.min(maxFail, 20); + LOG("Downloader:onStopRequest - status: " + status + ", " + + "current fail: " + this.updateService._consecutiveSocketErrors + ", " + + "max fail: " + maxFail + ", " + "retryTimeout: " + retryTimeout); + if (Components.isSuccessCode(status)) { + if (this._verifyDownload()) { + if (shouldUseService()) { + state = STATE_PENDING_SERVICE; + } else if (getElevationRequired()) { + state = STATE_PENDING_ELEVATE; + } else { + state = STATE_PENDING; + } + if (this.background) { + shouldShowPrompt = !getCanStageUpdates(); + } + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, AUSTLMY.DWNLD_SUCCESS); + + // Tell the updater.exe we're ready to apply. + writeStatusFile(getUpdatesDir(), state); + writeVersionFile(getUpdatesDir(), this._update.appVersion); + this._update.installDate = (new Date()).getTime(); + this._update.statusText = gUpdateBundle.GetStringFromName("installPending"); + } + else { + LOG("Downloader:onStopRequest - download verification failed"); + state = STATE_DOWNLOAD_FAILED; + status = Cr.NS_ERROR_CORRUPTED_CONTENT; + + // Yes, this code is a string. + const vfCode = "verification_failed"; + var message = getStatusTextFromCode(vfCode, vfCode); + this._update.statusText = message; + + if (this._update.isCompleteUpdate || this._update.patchCount != 2) + deleteActiveUpdate = true; + + // Destroy the updates directory, since we're done with it. + cleanUpUpdatesDir(); + } + } else if (status == Cr.NS_ERROR_OFFLINE) { + // Register an online observer to try again. + // The online observer will continue the incremental download by + // calling downloadUpdate on the active update which continues + // downloading the file from where it was. + LOG("Downloader:onStopRequest - offline, register online observer: true"); + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, + AUSTLMY.DWNLD_RETRY_OFFLINE); + shouldRegisterOnlineObserver = true; + deleteActiveUpdate = false; + // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED, + // NS_ERROR_NET_RESET and NS_ERROR_DOCUMENT_NOT_CACHED can be returned + // when disconnecting the internet while a download of a MAR is in + // progress. There may be others but I have not encountered them during + // testing. + } else if ((status == Cr.NS_ERROR_NET_TIMEOUT || + status == Cr.NS_ERROR_CONNECTION_REFUSED || + status == Cr.NS_ERROR_NET_RESET || + status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) && + this.updateService._consecutiveSocketErrors < maxFail) { + LOG("Downloader:onStopRequest - socket error, shouldRetrySoon: true"); + let dwnldCode = AUSTLMY.DWNLD_RETRY_CONNECTION_REFUSED; + if (status == Cr.NS_ERROR_NET_TIMEOUT) { + dwnldCode = AUSTLMY.DWNLD_RETRY_NET_TIMEOUT; + } else if (status == Cr.NS_ERROR_NET_RESET) { + dwnldCode = AUSTLMY.DWNLD_RETRY_NET_RESET; + } else if (status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) { + dwnldCode = AUSTLMY.DWNLD_ERR_DOCUMENT_NOT_CACHED; + } + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode); + shouldRetrySoon = true; + deleteActiveUpdate = false; + } else if (status != Cr.NS_BINDING_ABORTED && + status != Cr.NS_ERROR_ABORT) { + LOG("Downloader:onStopRequest - non-verification failure"); + let dwnldCode = AUSTLMY.DWNLD_ERR_BINDING_ABORTED; + if (status == Cr.NS_ERROR_ABORT) { + dwnldCode = AUSTLMY.DWNLD_ERR_ABORT; + } + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode); + + // Some sort of other failure, log this in the |statusText| property + state = STATE_DOWNLOAD_FAILED; + + // XXXben - if |request| (The Incremental Download) provided a means + // for accessing the http channel we could do more here. + + this._update.statusText = getStatusTextFromCode(status, + Cr.NS_BINDING_FAILED); + + if (AppConstants.platform == "gonk") { + // bug891009: On FirefoxOS, manaully retry OTA download will reuse + // the Update object. We need to remove selected patch so that download + // can be triggered again successfully. + this._update.selectedPatch.selected = false; + } + + // Destroy the updates directory, since we're done with it. + cleanUpUpdatesDir(); + + deleteActiveUpdate = true; + } + LOG("Downloader:onStopRequest - setting state to: " + state); + this._patch.state = state; + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + if (deleteActiveUpdate) { + this._update.installDate = (new Date()).getTime(); + um.activeUpdate = null; + } + else if (um.activeUpdate) { + um.activeUpdate.state = state; + } + um.saveUpdates(); + + // Only notify listeners about the stopped state if we + // aren't handling an internal retry. + if (!shouldRetrySoon && !shouldRegisterOnlineObserver) { + var listeners = this._listeners.concat(); + var listenerCount = listeners.length; + for (var i = 0; i < listenerCount; ++i) { + listeners[i].onStopRequest(request, context, status); + } + } + + this._request = null; + + if (state == STATE_DOWNLOAD_FAILED) { + var allFailed = true; + // Check if there is a complete update patch that can be downloaded. + if (!this._update.isCompleteUpdate && this._update.patchCount == 2) { + LOG("Downloader:onStopRequest - verification of patch failed, " + + "downloading complete update patch"); + this._update.isCompleteUpdate = true; + let updateStatus = this.downloadUpdate(this._update); + + if (updateStatus == STATE_NONE) { + cleanupActiveUpdate(); + } else { + allFailed = false; + } + } + + if (allFailed) { + LOG("Downloader:onStopRequest - all update patch downloads failed"); + // If the update UI is not open (e.g. the user closed the window while + // downloading) and if at any point this was a foreground download + // notify the user about the error. If the update was a background + // update there is no notification since the user won't be expecting it. + if (!Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME)) { + this._update.QueryInterface(Ci.nsIWritablePropertyBag); + if (this._update.getProperty("foregroundDownload") == "true") { + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(this._update); + } + } + + if (AppConstants.platform == "gonk") { + // We always forward errors in B2G, since Gaia controls the update UI + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(this._update); + } + + // Prevent leaking the update object (bug 454964). + this._update = null; + } + // A complete download has been initiated or the failure was handled. + return; + } + + if (state == STATE_PENDING || state == STATE_PENDING_SERVICE || + state == STATE_PENDING_ELEVATE) { + if (getCanStageUpdates()) { + LOG("Downloader:onStopRequest - attempting to stage update: " + + this._update.name); + + // Initiate the update in the background + try { + Cc["@mozilla.org/updates/update-processor;1"]. + createInstance(Ci.nsIUpdateProcessor). + processUpdate(this._update); + } catch (e) { + // Fail gracefully in case the application does not support the update + // processor service. + LOG("Downloader:onStopRequest - failed to stage update. Exception: " + + e); + if (this.background) { + shouldShowPrompt = true; + } + } + } + } + + // Do this after *everything* else, since it will likely cause the app + // to shut down. + if (shouldShowPrompt) { + // Notify the user that an update has been downloaded and is ready for + // installation (i.e. that they should restart the application). We do + // not notify on failed update attempts. + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateDownloaded(this._update, true); + } + + if (shouldRegisterOnlineObserver) { + LOG("Downloader:onStopRequest - Registering online observer"); + this.updateService._registerOnlineObserver(); + } else if (shouldRetrySoon) { + LOG("Downloader:onStopRequest - Retrying soon"); + this.updateService._consecutiveSocketErrors++; + if (this.updateService._retryTimer) { + this.updateService._retryTimer.cancel(); + } + this.updateService._retryTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.updateService._retryTimer.initWithCallback(function() { + this._attemptResume(); + }.bind(this.updateService), retryTimeout, Ci.nsITimer.TYPE_ONE_SHOT); + } else { + // Prevent leaking the update object (bug 454964) + this._update = null; + } + }, + + /** + * See nsIInterfaceRequestor.idl + */ + getInterface: function Downloader_getInterface(iid) { + // The network request may require proxy authentication, so provide the + // default nsIAuthPrompt if requested. + if (iid.equals(Ci.nsIAuthPrompt)) { + var prompt = Cc["@mozilla.org/network/default-auth-prompt;1"]. + createInstance(); + return prompt.QueryInterface(iid); + } + throw Cr.NS_NOINTERFACE; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver, + Ci.nsIProgressEventSink, + Ci.nsIInterfaceRequestor]) +}; + +/** + * UpdatePrompt + * An object which can prompt the user with information about updates, request + * action, etc. Embedding clients can override this component with one that + * invokes a native front end. + * @constructor + */ +function UpdatePrompt() { +} +UpdatePrompt.prototype = { + /** + * See nsIUpdateService.idl + */ + checkForUpdates: function UP_checkForUpdates() { + if (this._getAltUpdateWindow()) + return; + + this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME, + null, null); + }, + + /** + * See nsIUpdateService.idl + */ + showUpdateAvailable: function UP_showUpdateAvailable(update) { + if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) || + this._getUpdateWindow() || this._getAltUpdateWindow()) { + return; + } + + this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null, + UPDATE_WINDOW_NAME, "updatesavailable", update); + }, + + /** + * See nsIUpdateService.idl + */ + showUpdateDownloaded: function UP_showUpdateDownloaded(update, background) { + if (background && getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false)) { + return; + } + // Trigger the display of the hamburger menu badge. + Services.obs.notifyObservers(null, "update-downloaded", update.state); + + if (this._getAltUpdateWindow()) + return; + + if (background) { + this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null, + UPDATE_WINDOW_NAME, "finishedBackground", update); + } else { + this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, + UPDATE_WINDOW_NAME, "finishedBackground", update); + } + }, + + /** + * See nsIUpdateService.idl + */ + showUpdateError: function UP_showUpdateError(update) { + if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) || + this._getAltUpdateWindow()) + return; + + // In some cases, we want to just show a simple alert dialog. + // Replace with Array.prototype.includes when it has stabilized. + if (update.state == STATE_FAILED && + (WRITE_ERRORS.indexOf(update.errorCode) != -1 || + update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR || + update.errorCode == FOTA_GENERAL_ERROR || + update.errorCode == FOTA_FILE_OPERATION_ERROR || + update.errorCode == FOTA_RECOVERY_ERROR || + update.errorCode == FOTA_UNKNOWN_ERROR)) { + var title = gUpdateBundle.GetStringFromName("updaterIOErrorTitle"); + var text = gUpdateBundle.formatStringFromName("updaterIOErrorMsg", + [Services.appinfo.name, + Services.appinfo.name], 2); + Services.ww.getNewPrompter(null).alert(title, text); + return; + } + + if (update.errorCode == BACKGROUNDCHECK_MULTIPLE_FAILURES) { + this._showUIWhenIdle(null, URI_UPDATE_PROMPT_DIALOG, null, + UPDATE_WINDOW_NAME, null, update); + return; + } + + this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME, + "errors", update); + }, + + /** + * See nsIUpdateService.idl + */ + showUpdateHistory: function UP_showUpdateHistory(parent) { + this._showUI(parent, URI_UPDATE_HISTORY_DIALOG, "modal,dialog=yes", + "Update:History", null, null); + }, + + /** + * See nsIUpdateService.idl + */ + showUpdateElevationRequired: function UP_showUpdateElevationRequired() { + if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) || + this._getAltUpdateWindow()) { + return; + } + + let um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, + UPDATE_WINDOW_NAME, "finishedBackground", um.activeUpdate); + }, + + /** + * Returns the update window if present. + */ + _getUpdateWindow: function UP__getUpdateWindow() { + return Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME); + }, + + /** + * Returns an alternative update window if present. When a window with this + * windowtype is open the application update service won't open the normal + * application update user interface window. + */ + _getAltUpdateWindow: function UP__getAltUpdateWindow() { + let windowType = getPref("getCharPref", PREF_APP_UPDATE_ALTWINDOWTYPE, null); + if (!windowType) + return null; + return Services.wm.getMostRecentWindow(windowType); + }, + + /** + * Display the update UI after the prompt wait time has elapsed. + * @param parent + * A parent window, can be null + * @param uri + * The URI string of the dialog to show + * @param name + * The Window Name of the dialog to show, in case it is already open + * and can merely be focused + * @param page + * The page of the wizard to be displayed, if one is already open. + * @param update + * An update to pass to the UI in the window arguments. + * Can be null + */ + _showUnobtrusiveUI: function UP__showUnobUI(parent, uri, features, name, page, + update) { + var observer = { + updatePrompt: this, + service: null, + timer: null, + notify: function () { + // the user hasn't restarted yet => prompt when idle + this.service.removeObserver(this, "quit-application"); + // If the update window is already open skip showing the UI + if (this.updatePrompt._getUpdateWindow()) + return; + this.updatePrompt._showUIWhenIdle(parent, uri, features, name, page, update); + }, + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "quit-application": + if (this.timer) + this.timer.cancel(); + this.service.removeObserver(this, "quit-application"); + break; + } + } + }; + + // bug 534090 - show the UI for update available notifications when the + // the system has been idle for at least IDLE_TIME. + if (page == "updatesavailable") { + var idleService = Cc["@mozilla.org/widget/idleservice;1"]. + getService(Ci.nsIIdleService); + // Don't allow the preference to set a value greater than 600 seconds for the idle time. + const IDLE_TIME = Math.min(getPref("getIntPref", PREF_APP_UPDATE_IDLETIME, 60), 600); + if (idleService.idleTime / 1000 >= IDLE_TIME) { + this._showUI(parent, uri, features, name, page, update); + return; + } + } + + observer.service = Services.obs; + observer.service.addObserver(observer, "quit-application", false); + + // bug 534090 - show the UI when idle for update available notifications. + if (page == "updatesavailable") { + this._showUIWhenIdle(parent, uri, features, name, page, update); + return; + } + + // Give the user x seconds to react before prompting as defined by + // promptWaitTime + observer.timer = Cc["@mozilla.org/timer;1"]. + createInstance(Ci.nsITimer); + observer.timer.initWithCallback(observer, update.promptWaitTime * 1000, + observer.timer.TYPE_ONE_SHOT); + }, + + /** + * Show the UI when the user was idle + * @param parent + * A parent window, can be null + * @param uri + * The URI string of the dialog to show + * @param name + * The Window Name of the dialog to show, in case it is already open + * and can merely be focused + * @param page + * The page of the wizard to be displayed, if one is already open. + * @param update + * An update to pass to the UI in the window arguments. + * Can be null + */ + _showUIWhenIdle: function UP__showUIWhenIdle(parent, uri, features, name, + page, update) { + var idleService = Cc["@mozilla.org/widget/idleservice;1"]. + getService(Ci.nsIIdleService); + + // Don't allow the preference to set a value greater than 600 seconds for the idle time. + const IDLE_TIME = Math.min(getPref("getIntPref", PREF_APP_UPDATE_IDLETIME, 60), 600); + if (idleService.idleTime / 1000 >= IDLE_TIME) { + this._showUI(parent, uri, features, name, page, update); + } else { + var observer = { + updatePrompt: this, + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "idle": + // If the update window is already open skip showing the UI + if (!this.updatePrompt._getUpdateWindow()) + this.updatePrompt._showUI(parent, uri, features, name, page, update); + // fall thru + case "quit-application": + idleService.removeIdleObserver(this, IDLE_TIME); + Services.obs.removeObserver(this, "quit-application"); + break; + } + } + }; + idleService.addIdleObserver(observer, IDLE_TIME); + Services.obs.addObserver(observer, "quit-application", false); + } + }, + + /** + * Show the Update Checking UI + * @param parent + * A parent window, can be null + * @param uri + * The URI string of the dialog to show + * @param name + * The Window Name of the dialog to show, in case it is already open + * and can merely be focused + * @param page + * The page of the wizard to be displayed, if one is already open. + * @param update + * An update to pass to the UI in the window arguments. + * Can be null + */ + _showUI: function UP__showUI(parent, uri, features, name, page, update) { + var ary = null; + if (update) { + ary = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + ary.appendElement(update, /* weak =*/ false); + } + + var win = this._getUpdateWindow(); + if (win) { + if (page && "setCurrentPage" in win) + win.setCurrentPage(page); + win.focus(); + } + else { + var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no"; + if (features) + openFeatures += "," + features; + Services.ww.openWindow(parent, uri, "", openFeatures, ary); + } + }, + + classDescription: "Update Prompt", + contractID: "@mozilla.org/updates/update-prompt;1", + classID: Components.ID("{27ABA825-35B5-4018-9FDD-F99250A0E722}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePrompt]) +}; + +var components = [UpdateService, Checker, UpdatePrompt, UpdateManager]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/toolkit/mozapps/update/nsUpdateService.manifest b/toolkit/mozapps/update/nsUpdateService.manifest new file mode 100644 index 000000000..59dbf0b5a --- /dev/null +++ b/toolkit/mozapps/update/nsUpdateService.manifest @@ -0,0 +1,12 @@ +component {B3C290A6-3943-4B89-8BBE-C01EB7B3B311} nsUpdateService.js +contract @mozilla.org/updates/update-service;1 {B3C290A6-3943-4B89-8BBE-C01EB7B3B311} +category update-timer nsUpdateService @mozilla.org/updates/update-service;1,getService,background-update-timer,app.update.interval,43200,86400 +component {093C2356-4843-4C65-8709-D7DBCBBE7DFB} nsUpdateService.js +contract @mozilla.org/updates/update-manager;1 {093C2356-4843-4C65-8709-D7DBCBBE7DFB} +component {898CDC9B-E43F-422F-9CC4-2F6291B415A3} nsUpdateService.js +contract @mozilla.org/updates/update-checker;1 {898CDC9B-E43F-422F-9CC4-2F6291B415A3} +component {27ABA825-35B5-4018-9FDD-F99250A0E722} nsUpdateService.js +contract @mozilla.org/updates/update-prompt;1 {27ABA825-35B5-4018-9FDD-F99250A0E722} +component {e43b0010-04ba-4da6-b523-1f92580bc150} nsUpdateServiceStub.js +contract @mozilla.org/updates/update-service-stub;1 {e43b0010-04ba-4da6-b523-1f92580bc150} +category profile-after-change nsUpdateServiceStub @mozilla.org/updates/update-service-stub;1 diff --git a/toolkit/mozapps/update/nsUpdateServiceStub.js b/toolkit/mozapps/update/nsUpdateServiceStub.js new file mode 100644 index 000000000..dbb841b84 --- /dev/null +++ b/toolkit/mozapps/update/nsUpdateServiceStub.js @@ -0,0 +1,45 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/FileUtils.jsm", this); + +const DIR_UPDATES = "updates"; +const FILE_UPDATE_STATUS = "update.status"; + +const KEY_UPDROOT = "UpdRootD"; + +/** + * Gets the specified directory at the specified hierarchy under the update root + * directory without creating it if it doesn't exist. + * @param pathArray + * An array of path components to locate beneath the directory + * specified by |key| + * @return nsIFile object for the location specified. + */ +function getUpdateDirNoCreate(pathArray) { + return FileUtils.getDir(KEY_UPDROOT, pathArray, false); +} + +function UpdateServiceStub() { + let statusFile = getUpdateDirNoCreate([DIR_UPDATES, "0"]); + statusFile.append(FILE_UPDATE_STATUS); + // If the update.status file exists then initiate post update processing. + if (statusFile.exists()) { + let aus = Cc["@mozilla.org/updates/update-service;1"]. + getService(Ci.nsIApplicationUpdateService). + QueryInterface(Ci.nsIObserver); + aus.observe(null, "post-update-processing", ""); + } +} +UpdateServiceStub.prototype = { + observe: function() {}, + classID: Components.ID("{e43b0010-04ba-4da6-b523-1f92580bc150}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UpdateServiceStub]); diff --git a/toolkit/mozapps/update/tests/Makefile.in b/toolkit/mozapps/update/tests/Makefile.in new file mode 100644 index 000000000..0b8d19aa2 --- /dev/null +++ b/toolkit/mozapps/update/tests/Makefile.in @@ -0,0 +1,39 @@ +# 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/. + +XPCSHELLTESTROOT = $(topobjdir)/_tests/xpcshell/$(relativesrcdir) + +pp_const_file = $(srcdir)/data/xpcshellConstantsPP.js + +PP_TARGETS += aus-test-const +aus-test-const := $(pp_const_file) +aus-test-const_PATH := $(XPCSHELLTESTROOT)/data +aus-test-const_FLAGS := -Fsubstitution $(DEFINES) $(ACDEFINES) +aus-test-const_TARGET := misc + +INI_TEST_FILES = \ + TestAUSReadStrings1.ini \ + TestAUSReadStrings2.ini \ + TestAUSReadStrings3.ini \ + $(NULL) + +MOZ_WINCONSOLE = 1 + +include $(topsrcdir)/config/rules.mk + +# TestAUSReadStrings runs during check in the following directory with a Unicode +# char in order to test bug 473417 on Windows. +ifeq ($(OS_ARCH),WINNT) +bug473417dir = test_bug473417-� +else +bug473417dir = test_bug473417 +endif + +check:: + $(RM) -rf $(DEPTH)/_tests/updater/ && $(NSINSTALL) -D $(DEPTH)/_tests/updater/$(bug473417dir)/ + for i in $(INI_TEST_FILES); do \ + $(INSTALL) $(srcdir)/$$i $(DEPTH)/_tests/updater/$(bug473417dir)/; \ + done + $(INSTALL) $(FINAL_TARGET)/TestAUSReadStrings$(BIN_SUFFIX) $(DEPTH)/_tests/updater/$(bug473417dir)/ + @$(RUN_TEST_PROGRAM) $(DEPTH)/_tests/updater/$(bug473417dir)/TestAUSReadStrings$(BIN_SUFFIX) diff --git a/toolkit/mozapps/update/tests/TestAUSHelper.cpp b/toolkit/mozapps/update/tests/TestAUSHelper.cpp new file mode 100644 index 000000000..f71103b7a --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSHelper.cpp @@ -0,0 +1,423 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#ifdef XP_WIN +# include <windows.h> +# include <wintrust.h> +# include <tlhelp32.h> +# include <softpub.h> +# include <direct.h> +# include <io.h> + typedef WCHAR NS_tchar; +# define NS_main wmain +# ifndef F_OK +# define F_OK 00 +# endif +# ifndef W_OK +# define W_OK 02 +# endif +# ifndef R_OK +# define R_OK 04 +# endif +# if defined(_MSC_VER) && _MSC_VER < 1900 +# define stat _stat +# endif +# define NS_T(str) L ## str +# define NS_tsnprintf(dest, count, fmt, ...) \ + { \ + int _count = count - 1; \ + _snwprintf(dest, _count, fmt, ##__VA_ARGS__); \ + dest[_count] = L'\0'; \ + } +# define NS_taccess _waccess +# define NS_tchdir _wchdir +# define NS_tfopen _wfopen +# define NS_tstrcmp wcscmp +# define NS_ttoi _wtoi +# define NS_tstat _wstat +# define NS_tgetcwd _wgetcwd +# define LOG_S "%S" + +#include "../common/updatehelper.h" +#include "../common/certificatecheck.h" + +#else +# include <unistd.h> +# define NS_main main + typedef char NS_tchar; +# define NS_T(str) str +# define NS_tsnprintf snprintf +# define NS_taccess access +# define NS_tchdir chdir +# define NS_tfopen fopen +# define NS_tstrcmp strcmp +# define NS_ttoi atoi +# define NS_tstat stat +# define NS_tgetcwd getcwd +# define NS_tfputs fputs +# define LOG_S "%s" +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> + +#ifndef MAXPATHLEN +# ifdef PATH_MAX +# define MAXPATHLEN PATH_MAX +# elif defined(MAX_PATH) +# define MAXPATHLEN MAX_PATH +# elif defined(_MAX_PATH) +# define MAXPATHLEN _MAX_PATH +# elif defined(CCHMAXPATH) +# define MAXPATHLEN CCHMAXPATH +# else +# define MAXPATHLEN 1024 +# endif +#endif + +static void +WriteMsg(const NS_tchar *path, const char *status) +{ + FILE* outFP = NS_tfopen(path, NS_T("wb")); + if (!outFP) { + return; + } + + fprintf(outFP, "%s\n", status); + fclose(outFP); + outFP = nullptr; +} + +static bool +CheckMsg(const NS_tchar *path, const char *expected) +{ + if (NS_taccess(path, F_OK)) { + return false; + } + + FILE *inFP = NS_tfopen(path, NS_T("rb")); + if (!inFP) { + return false; + } + + struct stat ms; + if (fstat(fileno(inFP), &ms)) { + fclose(inFP); + inFP = nullptr; + return false; + } + + char *mbuf = (char *) malloc(ms.st_size + 1); + if (!mbuf) { + fclose(inFP); + inFP = nullptr; + return false; + } + + size_t r = ms.st_size; + char *rb = mbuf; + size_t c = fread(rb, sizeof(char), 50, inFP); + r -= c; + rb += c; + if (c == 0 && r) { + free(mbuf); + fclose(inFP); + inFP = nullptr; + return false; + } + mbuf[ms.st_size] = '\0'; + rb = mbuf; + + bool isMatch = strcmp(rb, expected) == 0; + free(mbuf); + fclose(inFP); + inFP = nullptr; + return isMatch; +} + +int NS_main(int argc, NS_tchar **argv) +{ + if (argc == 2) { + if (!NS_tstrcmp(argv[1], NS_T("post-update-async")) || + !NS_tstrcmp(argv[1], NS_T("post-update-sync"))) { + NS_tchar exePath[MAXPATHLEN]; +#ifdef XP_WIN + if (!::GetModuleFileNameW(0, exePath, MAXPATHLEN)) { + return 1; + } +#else + strcpy(exePath, argv[0]); +#endif + NS_tchar runFilePath[MAXPATHLEN]; + NS_tsnprintf(runFilePath, sizeof(runFilePath)/sizeof(runFilePath[0]), + NS_T("%s.running"), exePath); +#ifdef XP_WIN + if (!NS_taccess(runFilePath, F_OK)) { + // This makes it possible to check if the post update process was + // launched twice which happens when the service performs an update. + NS_tchar runFilePathBak[MAXPATHLEN]; + NS_tsnprintf(runFilePathBak, sizeof(runFilePathBak)/sizeof(runFilePathBak[0]), + NS_T("%s.bak"), runFilePath); + MoveFileExW(runFilePath, runFilePathBak, MOVEFILE_REPLACE_EXISTING); + } +#endif + WriteMsg(runFilePath, "running"); + + if (!NS_tstrcmp(argv[1], NS_T("post-update-sync"))) { +#ifdef XP_WIN + Sleep(2000); +#else + sleep(2); +#endif + } + + NS_tchar logFilePath[MAXPATHLEN]; + NS_tsnprintf(logFilePath, sizeof(logFilePath)/sizeof(logFilePath[0]), + NS_T("%s.log"), exePath); + WriteMsg(logFilePath, "post-update"); + return 0; + } + } + + if (argc < 3) { + fprintf(stderr, \ + "\n" \ + "Application Update Service Test Helper\n" \ + "\n" \ + "Usage: WORKINGDIR INFILE OUTFILE -s SECONDS [FILETOLOCK]\n" \ + " or: WORKINGDIR LOGFILE [ARG2 ARG3...]\n" \ + " or: signature-check filepath\n" \ + " or: setup-symlink dir1 dir2 file symlink\n" \ + " or: remove-symlink dir1 dir2 file symlink\n" \ + " or: check-symlink symlink\n" \ + " or: post-update\n" \ + "\n" \ + " WORKINGDIR \tThe relative path to the working directory to use.\n" \ + " INFILE \tThe relative path from the working directory for the file to\n" \ + " \tread actions to perform such as finish.\n" \ + " OUTFILE \tThe relative path from the working directory for the file to\n" \ + " \twrite status information.\n" \ + " SECONDS \tThe number of seconds to sleep.\n" \ + " FILETOLOCK \tThe relative path from the working directory to an existing\n" \ + " \tfile to open exlusively.\n" \ + " \tOnly available on Windows platforms and silently ignored on\n" \ + " \tother platforms.\n" \ + " LOGFILE \tThe relative path from the working directory to log the\n" \ + " \tcommand line arguments.\n" \ + " ARG2 ARG3...\tArguments to write to the LOGFILE after the preceding command\n" \ + " \tline arguments.\n" \ + "\n" \ + "Note: All paths must be relative.\n" \ + "\n"); + return 1; + } + + if (!NS_tstrcmp(argv[1], NS_T("check-signature"))) { +#if defined(XP_WIN) && defined(MOZ_MAINTENANCE_SERVICE) + if (ERROR_SUCCESS == VerifyCertificateTrustForFile(argv[2])) { + return 0; + } else { + return 1; + } +#else + // Not implemented on non-Windows platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("setup-symlink"))) { +#ifdef XP_UNIX + NS_tchar path[MAXPATHLEN]; + NS_tsnprintf(path, sizeof(path)/sizeof(path[0]), + NS_T("%s/%s"), NS_T("/tmp"), argv[2]); + mkdir(path, 0755); + NS_tsnprintf(path, sizeof(path)/sizeof(path[0]), + NS_T("%s/%s/%s"), NS_T("/tmp"), argv[2], argv[3]); + mkdir(path, 0755); + NS_tsnprintf(path, sizeof(path)/sizeof(path[0]), + NS_T("%s/%s/%s/%s"), NS_T("/tmp"), argv[2], argv[3], argv[4]); + FILE * file = NS_tfopen(path, NS_T("w")); + if (file) { + NS_tfputs(NS_T("test"), file); + fclose(file); + } + if (symlink(path, argv[5]) != 0) { + return 1; + } + NS_tsnprintf(path, sizeof(path)/sizeof(path[0]), + NS_T("%s/%s"), NS_T("/tmp"), argv[2]); + if (argc > 6 && !NS_tstrcmp(argv[6], NS_T("change-perm"))) { + chmod(path, 0644); + } + return 0; +#else + // Not implemented on non-Unix platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("remove-symlink"))) { +#ifdef XP_UNIX + NS_tchar path[MAXPATHLEN]; + NS_tsnprintf(path, sizeof(path)/sizeof(path[0]), + NS_T("%s/%s"), NS_T("/tmp"), argv[2]); + chmod(path, 0755); + NS_tsnprintf(path, sizeof(path)/sizeof(path[0]), + NS_T("%s/%s/%s/%s"), NS_T("/tmp"), argv[2], argv[3], argv[4]); + unlink(path); + NS_tsnprintf(path, sizeof(path)/sizeof(path[0]), + NS_T("%s/%s/%s"), NS_T("/tmp"), argv[2], argv[3]); + rmdir(path); + NS_tsnprintf(path, sizeof(path)/sizeof(path[0]), + NS_T("%s/%s"), NS_T("/tmp"), argv[2]); + rmdir(path); + return 0; +#else + // Not implemented on non-Unix platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("check-symlink"))) { +#ifdef XP_UNIX + struct stat ss; + lstat(argv[2], &ss); + return S_ISLNK(ss.st_mode) ? 0 : 1; +#else + // Not implemented on non-Unix platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("wait-for-service-stop"))) { +#ifdef XP_WIN + const int maxWaitSeconds = NS_ttoi(argv[3]); + LPCWSTR serviceName = argv[2]; + DWORD serviceState = WaitForServiceStop(serviceName, maxWaitSeconds); + if (SERVICE_STOPPED == serviceState) { + return 0; + } else { + return serviceState; + } +#else + // Not implemented on non-Windows platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("wait-for-application-exit"))) { +#ifdef XP_WIN + const int maxWaitSeconds = NS_ttoi(argv[3]); + LPCWSTR application = argv[2]; + DWORD ret = WaitForProcessExit(application, maxWaitSeconds); + if (ERROR_SUCCESS == ret) { + return 0; + } else if (WAIT_TIMEOUT == ret) { + return 1; + } else { + return 2; + } +#else + // Not implemented on non-Windows platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("is-process-running"))) { +#ifdef XP_WIN + LPCWSTR application = argv[2]; + return (ERROR_NOT_FOUND == IsProcessRunning(application)) ? 0 : 1; +#else + // Not implemented on non-Windows platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("launch-service"))) { +#ifdef XP_WIN + DWORD ret = LaunchServiceSoftwareUpdateCommand(argc - 2, (LPCWSTR *)argv + 2); + if (ret != ERROR_SUCCESS) { + // 192 is used to avoid reusing a possible return value from the call to + // WaitForServiceStop + return 0x000000C0; + } + // Wait a maximum of 120 seconds. + DWORD lastState = WaitForServiceStop(SVC_NAME, 120); + if (SERVICE_STOPPED == lastState) { + return 0; + } + return lastState; +#else + // Not implemented on non-Windows platforms + return 1; +#endif + } + + if (NS_tchdir(argv[1]) != 0) { + return 1; + } + + // File in use test helper section + if (!NS_tstrcmp(argv[4], NS_T("-s"))) { + NS_tchar *cwd = NS_tgetcwd(nullptr, 0); + NS_tchar inFilePath[MAXPATHLEN]; + NS_tsnprintf(inFilePath, sizeof(inFilePath)/sizeof(inFilePath[0]), + NS_T("%s/%s"), cwd, argv[2]); + NS_tchar outFilePath[MAXPATHLEN]; + NS_tsnprintf(outFilePath, sizeof(outFilePath)/sizeof(outFilePath[0]), + NS_T("%s/%s"), cwd, argv[3]); + + int seconds = NS_ttoi(argv[5]); +#ifdef XP_WIN + HANDLE hFile = INVALID_HANDLE_VALUE; + if (argc == 7) { + hFile = CreateFileW(argv[6], + DELETE | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, 0, nullptr); + if (hFile == INVALID_HANDLE_VALUE) { + WriteMsg(outFilePath, "error_locking"); + return 1; + } + } + + WriteMsg(outFilePath, "sleeping"); + int i = 0; + while (!CheckMsg(inFilePath, "finish\n") && i++ <= seconds) { + Sleep(1000); + } + + if (argc == 7) { + CloseHandle(hFile); + } +#else + WriteMsg(outFilePath, "sleeping"); + int i = 0; + while (!CheckMsg(inFilePath, "finish\n") && i++ <= seconds) { + sleep(1); + } +#endif + WriteMsg(outFilePath, "finished"); + return 0; + } + + { + // Command line argument test helper section + NS_tchar logFilePath[MAXPATHLEN]; + NS_tsnprintf(logFilePath, sizeof(logFilePath)/sizeof(logFilePath[0]), + NS_T("%s"), argv[2]); + + FILE* logFP = NS_tfopen(logFilePath, NS_T("wb")); + for (int i = 1; i < argc; ++i) { + fprintf(logFP, LOG_S "\n", argv[i]); + } + + fclose(logFP); + logFP = nullptr; + } + + return 0; +} diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings.cpp b/toolkit/mozapps/update/tests/TestAUSReadStrings.cpp new file mode 100644 index 000000000..c1de44f8e --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSReadStrings.cpp @@ -0,0 +1,172 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +/** + * This binary tests the updater's ReadStrings ini parser and should run in a + * directory with a Unicode character to test bug 473417. + */ +#ifdef XP_WIN + #include <windows.h> + #define NS_main wmain + #define NS_tstrrchr wcsrchr + #define NS_T(str) L ## str + #define PATH_SEPARATOR_CHAR L'\\' + // On Windows, argv[0] can also have forward slashes instead + #define ALT_PATH_SEPARATOR_CHAR L'/' +#else + #include <unistd.h> + #define NS_main main + #define NS_tstrrchr strrchr + #define NS_T(str) str + #define PATH_SEPARATOR_CHAR '/' +#endif + +#include <stdio.h> +#include <stdarg.h> +#include <string.h> + +#include "updater/resource.h" +#include "updater/progressui.h" +#include "common/readstrings.h" +#include "common/errors.h" +#include "mozilla/ArrayUtils.h" + +#ifndef MAXPATHLEN +# ifdef PATH_MAX +# define MAXPATHLEN PATH_MAX +# elif defined(MAX_PATH) +# define MAXPATHLEN MAX_PATH +# elif defined(_MAX_PATH) +# define MAXPATHLEN _MAX_PATH +# elif defined(CCHMAXPATH) +# define MAXPATHLEN CCHMAXPATH +# else +# define MAXPATHLEN 1024 +# endif +#endif + +#define TEST_NAME "Updater ReadStrings" + +using namespace mozilla; + +static int gFailCount = 0; + +/** + * Prints the given failure message and arguments using printf, prepending + * "TEST-UNEXPECTED-FAIL " for the benefit of the test harness and + * appending "\n" to eliminate having to type it at each call site. + */ +void fail(const char* msg, ...) +{ + va_list ap; + + printf("TEST-UNEXPECTED-FAIL | "); + + va_start(ap, msg); + vprintf(msg, ap); + va_end(ap); + + putchar('\n'); + ++gFailCount; +} + +int NS_main(int argc, NS_tchar **argv) +{ + printf("Running TestAUSReadStrings tests\n"); + + int rv = 0; + int retval; + NS_tchar inifile[MAXPATHLEN]; + StringTable testStrings; + + NS_tchar *slash = NS_tstrrchr(argv[0], PATH_SEPARATOR_CHAR); +#ifdef ALT_PATH_SEPARATOR_CHAR + NS_tchar *altslash = NS_tstrrchr(argv[0], ALT_PATH_SEPARATOR_CHAR); + slash = (slash > altslash) ? slash : altslash; +#endif // ALT_PATH_SEPARATOR_CHAR + + if (!slash) { + fail("%s | unable to find platform specific path separator (check 1)", TEST_NAME); + return 20; + } + + *(++slash) = '\0'; + // Test success when the ini file exists with both Title and Info in the + // Strings section and the values for Title and Info. + NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings1.ini"), argv[0]); + retval = ReadStrings(inifile, &testStrings); + if (retval == OK) { + if (strcmp(testStrings.title, "Title Test - \xD0\x98\xD1\x81\xD0\xBF\xD1\x8B" \ + "\xD1\x82\xD0\xB0\xD0\xBD\xD0\xB8\xD0\xB5 " \ + "\xCE\x94\xCE\xBF\xCE\xBA\xCE\xB9\xCE\xBC\xCE\xAE " \ + "\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88 " \ + "\xE6\xB8\xAC\xE8\xA9\xA6 " \ + "\xE6\xB5\x8B\xE8\xAF\x95") != 0) { + rv = 21; + fail("%s | Title ini value incorrect (check 3)", TEST_NAME); + } + + if (strcmp(testStrings.info, "Info Test - \xD0\x98\xD1\x81\xD0\xBF\xD1\x8B" \ + "\xD1\x82\xD0\xB0\xD0\xBD\xD0\xB8\xD0\xB5 " \ + "\xCE\x94\xCE\xBF\xCE\xBA\xCE\xB9\xCE\xBC\xCE\xAE " \ + "\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88 " \ + "\xE6\xB8\xAC\xE8\xA9\xA6 " \ + "\xE6\xB5\x8B\xE8\xAF\x95\xE2\x80\xA6") != 0) { + rv = 22; + fail("%s | Info ini value incorrect (check 4)", TEST_NAME); + } + } else { + fail("%s | ReadStrings returned %i (check 2)", TEST_NAME, retval); + rv = 23; + } + + // Test failure when the ini file exists without Title and with Info in the + // Strings section. + NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings2.ini"), argv[0]); + retval = ReadStrings(inifile, &testStrings); + if (retval != PARSE_ERROR) { + rv = 24; + fail("%s | ReadStrings returned %i (check 5)", TEST_NAME, retval); + } + + // Test failure when the ini file exists with Title and without Info in the + // Strings section. + NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings3.ini"), argv[0]); + retval = ReadStrings(inifile, &testStrings); + if (retval != PARSE_ERROR) { + rv = 25; + fail("%s | ReadStrings returned %i (check 6)", TEST_NAME, retval); + } + + // Test failure when the ini file doesn't exist + NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStringsBogus.ini"), argv[0]); + retval = ReadStrings(inifile, &testStrings); + if (retval != READ_ERROR) { + rv = 26; + fail("%s | ini file doesn't exist (check 7)", TEST_NAME); + } + + // Test reading a non-default section name + NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings3.ini"), argv[0]); + retval = ReadStrings(inifile, "Title\0", 1, &testStrings.title, "BogusSection2"); + if (retval == OK) { + if (strcmp(testStrings.title, "Bogus Title") != 0) { + rv = 27; + fail("%s | Title ini value incorrect (check 9)", TEST_NAME); + } + } else { + fail("%s | ReadStrings returned %i (check 8)", TEST_NAME, retval); + rv = 28; + } + + + if (rv == 0) { + printf("TEST-PASS | %s | all checks passed\n", TEST_NAME); + } else { + fail("%s | %i out of 9 checks failed", TEST_NAME, gFailCount); + } + + return rv; +} diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings1.ini b/toolkit/mozapps/update/tests/TestAUSReadStrings1.ini new file mode 100644 index 000000000..5ab13c185 --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSReadStrings1.ini @@ -0,0 +1,47 @@ +; This file is in the UTF-8 encoding + +[BogusSection1] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment + +[Strings] + +Bogus1=Bogus1 + +; Comment + +Title=Title Test - Испытание Δοκιμή テスト 測試 测试 + +; Comment + +Bogus2=Bogus2 + +; Comment + +Info=Info Test - Испытание Δοκιμή テスト 測試 测试… + +; Comment + +Bogus3=Bogus3 + +; Comment + +[BogusSection2] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings2.ini b/toolkit/mozapps/update/tests/TestAUSReadStrings2.ini new file mode 100644 index 000000000..8291a7c94 --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSReadStrings2.ini @@ -0,0 +1,39 @@ +; This file is in the UTF-8 encoding + +[BogusSection1] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment + +[Strings] + +Bogus1=Bogus1 + +; Comment + +Info=Info + +; Comment + +Bogus2=Bogus2 + +; Comment + +[BogusSection2] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings3.ini b/toolkit/mozapps/update/tests/TestAUSReadStrings3.ini new file mode 100644 index 000000000..a64d1232e --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSReadStrings3.ini @@ -0,0 +1,39 @@ +; This file is in the UTF-8 encoding + +[BogusSection1] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment + +[Strings] + +Bogus1=Bogus1 + +; Comment + +Title=Title + +; Comment + +Bogus2=Bogus2 + +; Comment + +[BogusSection2] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment diff --git a/toolkit/mozapps/update/tests/chrome/.eslintrc.js b/toolkit/mozapps/update/tests/chrome/.eslintrc.js new file mode 100644 index 000000000..8c0f4f574 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/chrome.eslintrc.js" + ] +}; diff --git a/toolkit/mozapps/update/tests/chrome/chrome.ini b/toolkit/mozapps/update/tests/chrome/chrome.ini new file mode 100644 index 000000000..88e3dd4e8 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/chrome.ini @@ -0,0 +1,64 @@ +; 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/. + +[DEFAULT] +tags = appupdate +support-files = + utils.js + update.sjs + +; mochitest-chrome tests must start with "test_" and are executed in sorted +; order and not in the order specified in the manifest. +[test_0010_background_basic.xul] +[test_0011_check_basic.xul] +[test_0012_check_basic_staging.xul] +skip-if = asan +reason = Bug 1168003 +[test_0013_check_no_updates.xul] +[test_0014_check_error_xml_malformed.xul] +[test_0061_check_verifyFailPartial_noComplete.xul] +[test_0062_check_verifyFailComplete_noPartial.xul] +[test_0063_check_verifyFailPartialComplete.xul] +[test_0064_check_verifyFailPartial_successComplete.xul] +[test_0071_notify_verifyFailPartial_noComplete.xul] +[test_0072_notify_verifyFailComplete_noPartial.xul] +[test_0073_notify_verifyFailPartialComplete.xul] +[test_0074_notify_verifyFailPartial_successComplete.xul] +[test_0081_error_patchApplyFailure_partial_only.xul] +[test_0082_error_patchApplyFailure_complete_only.xul] +[test_0083_error_patchApplyFailure_partial_complete.xul] +[test_0084_error_patchApplyFailure_verify_failed.xul] +[test_0085_error_patchApplyFailure_partial_complete_staging.xul] +skip-if = asan +reason = Bug 1168003 +[test_0092_finishedBackground.xul] +[test_0093_restartNotification.xul] +[test_0094_restartNotification_remote.xul] +[test_0095_restartNotification_remoteInvalidNumber.xul] +[test_0096_restartNotification_stagedBackground.xul] +skip-if = asan +reason = Bug 1168003 +[test_0097_restartNotification_stagedServiceBackground.xul] +skip-if = os != 'win' +reason = only Windows has the maintenance service. +[test_0101_background_restartNotification.xul] +[test_0102_background_restartNotification_staging.xul] +skip-if = asan +reason = Bug 1168003 +[test_0103_background_restartNotification_stagingService.xul] +skip-if = os != 'win' +reason = only Windows has the maintenance service. +[test_0111_neverButton_basic.xul] +[test_0113_showNeverForVersionRemovedWithPref.xul] +[test_0151_notify_backgroundCheckError.xul] +[test_0152_notify_backgroundCheckOfflineRetry.xul] +[test_0161_check_unsupported.xul] +[test_0162_notify_unsupported.xul] +[test_0171_check_noPerms_manual.xul] +skip-if = os != 'win' +reason = test must be able to prevent file deletion. +[test_0172_notify_noPerms_manual.xul] +skip-if = os != 'win' +reason = test must be able to prevent file deletion. +[test_9999_cleanup.xul] diff --git a/toolkit/mozapps/update/tests/chrome/test_0010_background_basic.xul b/toolkit/mozapps/update/tests/chrome/test_0010_background_basic.xul new file mode 100644 index 000000000..8d088cc8a --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0010_background_basic.xul @@ -0,0 +1,50 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: basic, download, and finished" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FOUND_BASIC, + buttonClick: "next" +}, { + pageid: PAGEID_DOWNLOADING +}, { + pageid: PAGEID_FINISHED, + buttonClick: "extra1" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1&showPrompt=1" + + getVersionParams(); + setUpdateURL(url); + + gAUS.checkForBackgroundUpdates(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0011_check_basic.xul b/toolkit/mozapps/update/tests/chrome/test_0011_check_basic.xul new file mode 100644 index 000000000..12b5302a4 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0011_check_basic.xul @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: update check, basic, download, and finished" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_FOUND_BASIC, + buttonClick: "next" +}, { + pageid: PAGEID_DOWNLOADING +}, { + pageid: PAGEID_FINISHED, + buttonClick: "extra1" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams(); + setUpdateURL(url); + + gUP.checkForUpdates(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0012_check_basic_staging.xul b/toolkit/mozapps/update/tests/chrome/test_0012_check_basic_staging.xul new file mode 100644 index 000000000..d910adc08 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0012_check_basic_staging.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: update check, basic, download with staging, and finished" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_FOUND_BASIC, + buttonClick: "next" +}, { + pageid: PAGEID_DOWNLOADING +}, { + pageid: PAGEID_FINISHED, + buttonClick: "extra1" +} ]; + +gUseTestUpdater = true; + +function runTest() { + debugDump("entering"); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams(); + setUpdateURL(url); + + gUP.checkForUpdates(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0013_check_no_updates.xul b/toolkit/mozapps/update/tests/chrome/test_0013_check_no_updates.xul new file mode 100644 index 000000000..c3f024c73 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0013_check_no_updates.xul @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: update check and no updates found" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_NO_UPDATES_FOUND, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?noUpdates=1"; + setUpdateURL(url); + + gUP.checkForUpdates(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0014_check_error_xml_malformed.xul b/toolkit/mozapps/update/tests/chrome/test_0014_check_error_xml_malformed.xul new file mode 100644 index 000000000..f399a0096 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0014_check_error_xml_malformed.xul @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: update check and error (xml malformed)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_ERRORS, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?xmlMalformed=1"; + setUpdateURL(url); + + gUP.checkForUpdates(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0061_check_verifyFailPartial_noComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0061_check_verifyFailPartial_noComplete.xul new file mode 100644 index 000000000..1040c19e3 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0061_check_verifyFailPartial_noComplete.xul @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: update check, basic, download, and errors (partial patch with an invalid size)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_FOUND_BASIC, + buttonClick: "next" +}, { + pageid: PAGEID_DOWNLOADING +}, { + pageid: PAGEID_ERRORS, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1&partialPatchOnly=1" + + "&invalidPartialSize=1" + getVersionParams(); + setUpdateURL(url); + + gUP.checkForUpdates(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0062_check_verifyFailComplete_noPartial.xul b/toolkit/mozapps/update/tests/chrome/test_0062_check_verifyFailComplete_noPartial.xul new file mode 100644 index 000000000..9221a4b98 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0062_check_verifyFailComplete_noPartial.xul @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: update check, basic, download, and errors (complete patch with an invalid size)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_FOUND_BASIC, + buttonClick: "next" +}, { + pageid: PAGEID_DOWNLOADING +}, { + pageid: PAGEID_ERRORS, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1&completePatchOnly=1" + + "&invalidCompleteSize=1" + getVersionParams(); + setUpdateURL(url); + + gUP.checkForUpdates(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0063_check_verifyFailPartialComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0063_check_verifyFailPartialComplete.xul new file mode 100644 index 000000000..8da5c7e97 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0063_check_verifyFailPartialComplete.xul @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: update check, basic, download, and errors (partial and complete patches with invalid sizes)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_FOUND_BASIC, + buttonClick: "next" +}, { + pageid: PAGEID_DOWNLOADING +}, { + pageid: PAGEID_ERRORS, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1&invalidPartialSize=1" + + "&invalidCompleteSize=1" + getVersionParams(); + setUpdateURL(url); + + gUP.checkForUpdates(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0064_check_verifyFailPartial_successComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0064_check_verifyFailPartial_successComplete.xul new file mode 100644 index 000000000..db0f33d2f --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0064_check_verifyFailPartial_successComplete.xul @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: update check, basic, download, and finished (partial patch with an invalid size and successful complete patch)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_FOUND_BASIC, + buttonClick: "next" +}, { + pageid: PAGEID_DOWNLOADING +}, { + pageid: PAGEID_FINISHED, + buttonClick: "extra1" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1&invalidPartialSize=1" + + getVersionParams(); + setUpdateURL(url); + + gUP.checkForUpdates(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0071_notify_verifyFailPartial_noComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0071_notify_verifyFailPartial_noComplete.xul new file mode 100644 index 000000000..736df13a3 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0071_notify_verifyFailPartial_noComplete.xul @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: errors (partial patch with an invalid size)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_ERRORS, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("partial", null, null, null, "1234", null, + STATE_DOWNLOADING); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, + Services.appinfo.platformVersion); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_DOWNLOADING); + reloadUpdateManagerData(); + + testPostUpdateProcessing(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0072_notify_verifyFailComplete_noPartial.xul b/toolkit/mozapps/update/tests/chrome/test_0072_notify_verifyFailComplete_noPartial.xul new file mode 100644 index 000000000..cafab4d27 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0072_notify_verifyFailComplete_noPartial.xul @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: errors (complete patch with an invalid size)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_ERRORS, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("complete", null, null, null, "1234", null, + STATE_DOWNLOADING); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_DOWNLOADING); + reloadUpdateManagerData(); + + testPostUpdateProcessing(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0073_notify_verifyFailPartialComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0073_notify_verifyFailPartialComplete.xul new file mode 100644 index 000000000..c1db983a3 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0073_notify_verifyFailPartialComplete.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: errors (partial and complete patches with invalid sizes)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_ERRORS, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("partial", null, null, null, "1234", null, + STATE_DOWNLOADING) + + getLocalPatchString("complete", null, null, null, "1234", + "false"); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, null, + null, null, null, null, "false"); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_DOWNLOADING); + reloadUpdateManagerData(); + + testPostUpdateProcessing(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0074_notify_verifyFailPartial_successComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0074_notify_verifyFailPartial_successComplete.xul new file mode 100644 index 000000000..2c28da768 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0074_notify_verifyFailPartial_successComplete.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: finishedBackground (partial patch with an invalid size and successful complete patch)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FINISHED_BKGRD, + buttonClick: "extra1" +} ]; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("partial", null, null, null, "1234", null, + STATE_DOWNLOADING) + + getLocalPatchString("complete", null, null, null, null, + "false"); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, null, + null, null, null, null, "false"); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_DOWNLOADING); + reloadUpdateManagerData(); + + testPostUpdateProcessing(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0081_error_patchApplyFailure_partial_only.xul b/toolkit/mozapps/update/tests/chrome/test_0081_error_patchApplyFailure_partial_only.xul new file mode 100644 index 000000000..10c34f63b --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0081_error_patchApplyFailure_partial_only.xul @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: errors (partial only patch apply failure)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_ERRORS, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("partial", null, null, null, null, null, + STATE_PENDING); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, null, + null, null, null, null, "false"); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_FAILED_CRC_ERROR); + reloadUpdateManagerData(); + + testPostUpdateProcessing(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0082_error_patchApplyFailure_complete_only.xul b/toolkit/mozapps/update/tests/chrome/test_0082_error_patchApplyFailure_complete_only.xul new file mode 100644 index 000000000..2c4b389f4 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0082_error_patchApplyFailure_complete_only.xul @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: errors (complete only patch apply failure)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_ERRORS, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("complete", null, null, null, null, null, + STATE_PENDING); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_FAILED_CRC_ERROR); + reloadUpdateManagerData(); + + testPostUpdateProcessing(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul b/toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul new file mode 100644 index 000000000..01adb1c3d --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul @@ -0,0 +1,67 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: error patching, download, and finished (partial failed and download complete)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_ERROR_PATCHING, + buttonClick: "next" +}, { + pageid: PAGEID_DOWNLOADING, + extraStartFunction: createContinueFile +}, { + pageid: PAGEID_FINISHED, + buttonClick: "extra1", + extraStartFunction: removeContinueFile +} ]; + +function runTest() { + debugDump("entering"); + + removeContinueFile(); + + // Specify the url to update.sjs with a slowDownloadMar param so the ui can + // load before the download completes. + let slowDownloadURL = URL_HTTP_UPDATE_XML + "?slowDownloadMar=1"; + let patches = getLocalPatchString("partial", null, null, null, null, null, + STATE_PENDING) + + getLocalPatchString("complete", slowDownloadURL, null, null, + null, "false"); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, null, + null, null, null, null, "false"); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_FAILED_CRC_ERROR); + reloadUpdateManagerData(); + + testPostUpdateProcessing(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul b/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul new file mode 100644 index 000000000..2e0c2b41e --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: error patching, download, and errors (partial failed and download complete verification failure)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_ERROR_PATCHING, + buttonClick: "next" +}, { + pageid: PAGEID_DOWNLOADING, + extraStartFunction: createContinueFile +}, { + pageid: PAGEID_ERRORS, + buttonClick: "finish", + extraStartFunction: removeContinueFile +} ]; + +function runTest() { + debugDump("entering"); + + removeContinueFile(); + + // Specify the url to update.sjs with a slowDownloadMar param so the ui can + // load before the download completes. + let slowDownloadURL = URL_HTTP_UPDATE_XML + "?slowDownloadMar=1"; + let patches = getLocalPatchString("partial", null, null, null, null, null, + STATE_PENDING) + + getLocalPatchString("complete", slowDownloadURL, "MD5", + null, "1234", + "false"); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, null, + null, null, null, null, "false"); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_FAILED_CRC_ERROR); + reloadUpdateManagerData(); + + testPostUpdateProcessing(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0085_error_patchApplyFailure_partial_complete_staging.xul b/toolkit/mozapps/update/tests/chrome/test_0085_error_patchApplyFailure_partial_complete_staging.xul new file mode 100644 index 000000000..fc83505f9 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0085_error_patchApplyFailure_partial_complete_staging.xul @@ -0,0 +1,94 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: error patching, download with staging, and finished (partial failed and download complete), with fast MAR download" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +// This test forces the download to complete before the "next" button on the +// errorpatching wizard page is clicked. This is done by creating the continue +// file when the wizard loads to start the download, then clicking the "next" +// button in the download's onStopRequest event listener. + +const testDownloadListener = { + onStartRequest(aRequest, aContext) { }, + + onProgress(aRequest, aContext, aProgress, aMaxProgress) { }, + + onStatus(aRequest, aContext, aStatus, aStatusText) { }, + + onStopRequest(aRequest, aContext, aStatus) { + debugDump("clicking errorpatching page next button"); + gDocElem.getButton("next").click(); + gAUS.removeDownloadListener(this); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver, + Ci.nsIProgressEventSink]) +}; + +let TESTS = [ { + pageid: PAGEID_ERROR_PATCHING, + extraCheckFunction: createContinueFile +}, { + pageid: PAGEID_DOWNLOADING +}, { + pageid: PAGEID_FINISHED, + buttonClick: "extra1", + extraStartFunction: removeContinueFile +} ]; + +gUseTestUpdater = true; + +function runTest() { + debugDump("entering"); + + removeContinueFile(); + + // Specify the url to update.sjs with a slowDownloadMar param so the ui can + // load before the download completes. + let slowDownloadURL = URL_HTTP_UPDATE_XML + "?slowDownloadMar=1"; + let patches = getLocalPatchString("partial", null, null, null, null, null, + STATE_PENDING) + + getLocalPatchString("complete", slowDownloadURL, null, null, + null, "false"); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, null, + null, null, null, null, "false"); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_FAILED_READ_ERROR); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true); + + reloadUpdateManagerData(); + + testPostUpdateProcessing(); + + gAUS.addDownloadListener(testDownloadListener); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0092_finishedBackground.xul b/toolkit/mozapps/update/tests/chrome/test_0092_finishedBackground.xul new file mode 100644 index 000000000..a0b29ddea --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0092_finishedBackground.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: finished background" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FINISHED_BKGRD, + buttonClick: "extra1" +} ]; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("complete", null, null, null, null, null, + STATE_PENDING); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, + Services.appinfo.platformVersion); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_SUCCEEDED); + + reloadUpdateManagerData(); + + is(gUpdateManager.activeUpdate.state, "pending", + "The active update should have a state of pending"); + + gUP.showUpdateDownloaded(gUpdateManager.activeUpdate); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0093_restartNotification.xul b/toolkit/mozapps/update/tests/chrome/test_0093_restartNotification.xul new file mode 100644 index 000000000..6db5b9897 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0093_restartNotification.xul @@ -0,0 +1,60 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: restart notification pref promptWaitTime" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FINISHED_BKGRD, + buttonClick: "extra1" +} ]; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("complete", null, null, null, null, null, + STATE_PENDING); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_SUCCEEDED); + + Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1); + + reloadUpdateManagerData(); + + is(gUpdateManager.activeUpdate.state, STATE_PENDING, + "The active update should have a state of " + STATE_PENDING); + + ok(gUpdateManager.activeUpdate.promptWaitTime == 1, "Checking that the " + + "update's promptWaitTime attribute value was set from the " + + PREF_APP_UPDATE_PROMPTWAITTIME + " preference"); + + gUP.showUpdateDownloaded(gUpdateManager.activeUpdate, true); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0094_restartNotification_remote.xul b/toolkit/mozapps/update/tests/chrome/test_0094_restartNotification_remote.xul new file mode 100644 index 000000000..6e72a42c1 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0094_restartNotification_remote.xul @@ -0,0 +1,60 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: restart notification xml promptWaitTime" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FINISHED_BKGRD, + buttonClick: "extra1" +} ]; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("complete", null, null, null, null, null, + STATE_PENDING); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, null, + null, null, null, null, false, + null, false, false, false, 1); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_SUCCEEDED); + + reloadUpdateManagerData(); + + is(gUpdateManager.activeUpdate.state, STATE_PENDING, + "The active update should have a state of " + STATE_PENDING); + + ok(gUpdateManager.activeUpdate.promptWaitTime == 1, "Checking that the " + + "update's promptWaitTime attribute value was set by the XML"); + + gUP.showUpdateDownloaded(gUpdateManager.activeUpdate, true); + +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0095_restartNotification_remoteInvalidNumber.xul b/toolkit/mozapps/update/tests/chrome/test_0095_restartNotification_remoteInvalidNumber.xul new file mode 100644 index 000000000..5b1b826a5 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0095_restartNotification_remoteInvalidNumber.xul @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: restart notification xml promptWaitTime with invalid number" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FINISHED_BKGRD, + buttonClick: "extra1" +} ]; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("complete", null, null, null, null, null, + STATE_PENDING); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, + null, null, + null, null, null, + null, false, null, + false, false, + false, "invalidNumber"); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_SUCCEEDED); + + Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1); + + reloadUpdateManagerData(); + + is(gUpdateManager.activeUpdate.state, STATE_PENDING, + "The active update should have a state of " + STATE_PENDING); + + ok(gUpdateManager.activeUpdate.promptWaitTime == 1, "Checking that the " + + "update's promptWaitTime attribute value was set from the " + + PREF_APP_UPDATE_PROMPTWAITTIME + " preference"); + + gUP.showUpdateDownloaded(gUpdateManager.activeUpdate, true); + +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0096_restartNotification_stagedBackground.xul b/toolkit/mozapps/update/tests/chrome/test_0096_restartNotification_stagedBackground.xul new file mode 100644 index 000000000..b86861012 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0096_restartNotification_stagedBackground.xul @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: restart notification staged w/o service" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FINISHED_BKGRD, + buttonClick: "extra1" +} ]; + +gUseTestUpdater = true; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("complete", null, null, null, null, null, + STATE_APPLIED); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, + Services.appinfo.platformVersion); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_APPLIED); + + Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1); + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true); + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false); + + reloadUpdateManagerData(); + + is(gUpdateManager.activeUpdate.state, STATE_APPLIED, + "The active update should have a state of " + STATE_APPLIED); + + ok(gUpdateManager.activeUpdate.promptWaitTime == 1, "Checking that the " + + "update's promptWaitTime attribute value was set from the " + + PREF_APP_UPDATE_PROMPTWAITTIME + " preference"); + + gUpdateManager.refreshUpdateStatus(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0097_restartNotification_stagedServiceBackground.xul b/toolkit/mozapps/update/tests/chrome/test_0097_restartNotification_stagedServiceBackground.xul new file mode 100644 index 000000000..9f7a602c4 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0097_restartNotification_stagedServiceBackground.xul @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: restart notification staged with service" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FINISHED_BKGRD, + buttonClick: "extra1" +} ]; + +gUseTestUpdater = true; + +function runTest() { + debugDump("entering"); + + let patches = getLocalPatchString("complete", null, null, null, null, null, + STATE_APPLIED_SVC); + let updates = getLocalUpdateString(patches, null, null, null, + Services.appinfo.version, + Services.appinfo.platformVersion); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + writeStatusFile(STATE_APPLIED_SVC); + + Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1); + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true); + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, true); + + reloadUpdateManagerData(); + + is(gUpdateManager.activeUpdate.state, STATE_APPLIED_SVC, + "The active update should have a state of " + STATE_APPLIED_SVC); + + ok(gUpdateManager.activeUpdate.promptWaitTime == 1, "Checking that the " + + "update's promptWaitTime attribute value was set from the " + + PREF_APP_UPDATE_PROMPTWAITTIME + " preference"); + + gUpdateManager.refreshUpdateStatus(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0101_background_restartNotification.xul b/toolkit/mozapps/update/tests/chrome/test_0101_background_restartNotification.xul new file mode 100644 index 000000000..faa60c08b --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0101_background_restartNotification.xul @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: background finish with a background download" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FINISHED_BKGRD, + buttonClick: "extra1" +} ]; + +function runTest() { + debugDump("entering"); + + Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams(); + setUpdateURL(url); + + gAUS.notify(null); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0102_background_restartNotification_staging.xul b/toolkit/mozapps/update/tests/chrome/test_0102_background_restartNotification_staging.xul new file mode 100644 index 000000000..3e6f0fec8 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0102_background_restartNotification_staging.xul @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: background finish with a background download and update staging" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FINISHED_BKGRD, + buttonClick: "extra1" +} ]; + +gUseTestUpdater = true; + +function runTest() { + debugDump("entering"); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true); + Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams(); + setUpdateURL(url); + + gAUS.notify(null); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0103_background_restartNotification_stagingService.xul b/toolkit/mozapps/update/tests/chrome/test_0103_background_restartNotification_stagingService.xul new file mode 100644 index 000000000..c60a9fe49 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0103_background_restartNotification_stagingService.xul @@ -0,0 +1,50 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: background finish with a background download and update staging and servicefs" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FINISHED_BKGRD, + buttonClick: "extra1" +} ]; + +gUseTestUpdater = true; + +function runTest() { + debugDump("entering"); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true); + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, true); + Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams(); + setUpdateURL(url); + + gAUS.notify(null); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0111_neverButton_basic.xul b/toolkit/mozapps/update/tests/chrome/test_0111_neverButton_basic.xul new file mode 100644 index 000000000..adca621d9 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0111_neverButton_basic.xul @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: update check and basic (never button test)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +gPrefToCheck = PREFBRANCH_APP_UPDATE_NEVER + Services.appinfo.version; + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_FOUND_BASIC, + extraDelayedCheckFunction: checkPrefHasUserValue, + prefHasUserValue: false, + neverButton: true, + buttonClick: "extra2" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?showNever=1&showDetails=1" + + getVersionParams(); + setUpdateURL(url); + + // add the never preference for this version to verify that checking for + // updates clears the preference. + Services.prefs.setBoolPref(gPrefToCheck, true) + + gUP.checkForUpdates(); +} + +function finishTest() { + checkPrefHasUserValue(true); + finishTestDefault(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0113_showNeverForVersionRemovedWithPref.xul b/toolkit/mozapps/update/tests/chrome/test_0113_showNeverForVersionRemovedWithPref.xul new file mode 100644 index 000000000..89dd55ea1 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0113_showNeverForVersionRemovedWithPref.xul @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: update available with never pref and without showNeverForVersion" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +gPrefToCheck = PREFBRANCH_APP_UPDATE_NEVER + Services.appinfo.version; + +const TESTS = [ { + pageid: PAGEID_FOUND_BASIC, + extraDelayedCheckFunction: checkPrefHasUserValue, + prefHasUserValue: true, + buttonClick: "extra1" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1&showPrompt=1" + + getVersionParams(); + setUpdateURL(url); + + // add the never preference for this version to verify that checking for + // updates clears the preference. + Services.prefs.setBoolPref(gPrefToCheck, true) + + gAUS.notify(null); +} + +function finishTest() { + Services.prefs.clearUserPref(gPrefToCheck) + finishTestDefault(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0151_notify_backgroundCheckError.xul b/toolkit/mozapps/update/tests/chrome/test_0151_notify_backgroundCheckError.xul new file mode 100644 index 000000000..13798e5c9 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0151_notify_backgroundCheckError.xul @@ -0,0 +1,50 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Test notification when multiple background check errors occur (bug 595455)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_ERROR_EXTRA, + extraDelayedCheckFunction: checkErrorExtraPage, + shouldBeHidden: false, + displayedTextElem: "bgErrorLabel", + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?xmlMalformed=1"; + setUpdateURL(url); + + errorsPrefObserver.init(PREF_APP_UPDATE_BACKGROUNDERRORS, + PREF_APP_UPDATE_BACKGROUNDMAXERRORS); + + gAUS.notify(null); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0152_notify_backgroundCheckOfflineRetry.xul b/toolkit/mozapps/update/tests/chrome/test_0152_notify_backgroundCheckOfflineRetry.xul new file mode 100644 index 000000000..04e613418 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0152_notify_backgroundCheckOfflineRetry.xul @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Test that an update check that fails due to being offline is performed after going online" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_FOUND_BASIC, + buttonClick: "extra1" +} ]; + +const NETWORK_ERROR_OFFLINE = 111; +var gProxyPrefValue; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams(); + setUpdateURL(url); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, true); + Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO, false); + + Services.io.offline = true; + gProxyPrefValue = Services.prefs.getIntPref("network.proxy.type"); + Services.prefs.setIntPref("network.proxy.type", 0); + + gUpdateChecker.checkForUpdates(updateCheckListener, true); +} + +function resetOffline() { + Services.prefs.setIntPref("network.proxy.type", gProxyPrefValue); + Services.io.offline = false; +} + +/* Update check listener */ +const updateCheckListener = { + onProgress: function UCL_onProgress(aRequest, aPosition, aTotalSize) { + }, + + onCheckComplete: function UCL_onCheckComplete(aRequest, aUpdates, aUpdateCount) { + let status = aRequest.status; + if (status == 0) { + status = aRequest.channel.QueryInterface(Ci.nsIRequest).status; + } + debugDump("url = " + aRequest.channel.originalURI.spec + ", " + + "request.status = " + status + ", " + + "updateCount = " + aUpdateCount); + ok(false, "Unexpected updateCheckListener::onCheckComplete called"); + }, + + onError: function UCL_onError(aRequest, aUpdate) { + let status = aRequest.status; + if (status == 0) { + status = aRequest.channel.QueryInterface(Ci.nsIRequest).status; + } + is(status, Cr.NS_ERROR_OFFLINE, + "checking the request status value"); + is(aUpdate.errorCode, NETWORK_ERROR_OFFLINE, + "checking the update error code"); + debugDump("url = " + aRequest.channel.originalURI.spec + ", " + + "request.status = " + status + ", " + + "update.statusText = " + + (aUpdate.statusText ? aUpdate.statusText : "null")); + gAUS.onError(aRequest, aUpdate); + // Use a timeout to allow the XHR to complete + SimpleTest.executeSoon(resetOffline); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateCheckListener]) +}; + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0161_check_unsupported.xul b/toolkit/mozapps/update/tests/chrome/test_0161_check_unsupported.xul new file mode 100644 index 000000000..c8e8d837b --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0161_check_unsupported.xul @@ -0,0 +1,50 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Test checking for updates when system is no longer supported (bug 843497)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_UNSUPPORTED, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + // When checking manually the unsupported page should still be shown even if + // it was shown previously. + Services.prefs.setBoolPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, true); + + let url = URL_HTTP_UPDATE_XML + "?unsupported=1"; + setUpdateURL(url); + + gUP.checkForUpdates(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0162_notify_unsupported.xul b/toolkit/mozapps/update/tests/chrome/test_0162_notify_unsupported.xul new file mode 100644 index 000000000..d88d2092d --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0162_notify_unsupported.xul @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Test notification of updates when system is no longer supported (bug 843497)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_UNSUPPORTED, + buttonClick: "finish" +} ]; + +function runTest() { + debugDump("entering"); + + let url = URL_HTTP_UPDATE_XML + "?unsupported=1"; + setUpdateURL(url); + + gAUS.notify(null); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0171_check_noPerms_manual.xul b/toolkit/mozapps/update/tests/chrome/test_0171_check_noPerms_manual.xul new file mode 100644 index 000000000..142c02baa --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0171_check_noPerms_manual.xul @@ -0,0 +1,64 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: manual" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_CHECKING +}, { + pageid: PAGEID_MANUAL_UPDATE, + buttonClick: "finish", + extraCheckFunction: getWriteTestFile +} ]; + +function runTest() { + debugDump("entering"); + + let file = getWriteTestFile(); + file.create(file.NORMAL_FILE_TYPE, 0o444); + file.fileAttributesWin |= file.WFA_READONLY; + file.fileAttributesWin &= ~file.WFA_READWRITE; + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams(); + setUpdateURL(url); + + gUP.checkForUpdates(); +} + +function getWriteTestFile() { + let file = getAppBaseDir(); + file.append(FILE_UPDATE_TEST); + file.QueryInterface(Ci.nsILocalFileWin); + if (file.exists()) { + file.fileAttributesWin |= file.WFA_READWRITE; + file.fileAttributesWin &= ~file.WFA_READONLY; + file.remove(true); + } + return file; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_0172_notify_noPerms_manual.xul b/toolkit/mozapps/update/tests/chrome/test_0172_notify_noPerms_manual.xul new file mode 100644 index 000000000..6784b9c90 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_0172_notify_noPerms_manual.xul @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Update Wizard pages: manual" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTestDefault();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +const TESTS = [ { + pageid: PAGEID_MANUAL_UPDATE, + buttonClick: "finish", + extraCheckFunction: getWriteTestFile +} ]; + +function runTest() { + debugDump("entering"); + + let file = getWriteTestFile(); + file.create(file.NORMAL_FILE_TYPE, 0o444); + file.fileAttributesWin |= file.WFA_READONLY; + file.fileAttributesWin &= ~file.WFA_READWRITE; + + let url = URL_HTTP_UPDATE_XML + "?showDetails=1&showPrompt=1" + + getVersionParams(); + setUpdateURL(url); + + gAUS.checkForBackgroundUpdates(); +} + +function getWriteTestFile() { + let file = getAppBaseDir(); + file.append(FILE_UPDATE_TEST); + file.QueryInterface(Ci.nsILocalFileWin); + if (file.exists()) { + file.fileAttributesWin |= file.WFA_READWRITE; + file.fileAttributesWin &= ~file.WFA_READONLY; + file.remove(true); + } + return file; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/test_9999_cleanup.xul b/toolkit/mozapps/update/tests/chrome/test_9999_cleanup.xul new file mode 100644 index 000000000..a55263eb2 --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/test_9999_cleanup.xul @@ -0,0 +1,112 @@ +<?xml version="1.0"?> +<!-- +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Removes files and preferences for previous application update tests in case + * any of them had a fatal error. The test name ensures that it will run after + * all other tests as long as the test naming uses the same format as the + * existing tests. + */ +--> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Application Update test cleanup" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest();"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="utils.js"/> + +<script type="application/javascript"> +<![CDATA[ + +/** + * If the application update tests left behind any of the files it uses it could + * be a very bad thing. The purpose of this test is to prevent that from + * happening. + */ +function runTest() { + debugDump("entering"); + + SimpleTest.waitForExplicitFinish(); + + if (DEBUG_AUS_TEST) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, true); + } + + closeUpdateWindow(); + + // Always leave the app.update.enabled and app.update.staging.enabled + // preferences set to false when cleaning up. + Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, false); + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false); + + resetFiles(); + removeUpdateDirsAndFiles(); + reloadUpdateManagerData(); + + let file = getUpdatesXMLFile(true); + ok(!file.exists(), file.path + " should not exist"); + + file = getUpdatesXMLFile(false); + ok(!file.exists(), file.path + " should not exist"); + + let dir = getUpdatesDir(); + + file = dir.clone(); + file.append(FILE_UPDATE_STATUS); + ok(!file.exists(), file.path + " should not exist"); + + file = dir.clone(); + file.append(FILE_UPDATE_MAR); + ok(!file.exists(), file.path + " should not exist"); + + cleanupRestoreUpdaterBackup(); +} + +/** + * After all tests finish this will repeatedly attempt to restore the real + * updater if it exists and then call finishTest after the restore is + * successful. + */ +function cleanupRestoreUpdaterBackup() { + debugDump("entering"); + + try { + // Windows debug builds keep the updater file in use for a short period of + // time after the updater process exits. + restoreUpdaterBackup(); + } catch (e) { + logTestInfo("Attempt to restore the backed up updater failed... " + + "will try again, Exception: " + e); + SimpleTest.executeSoon(cleanupRestoreUpdaterBackup); + return; + } + + SimpleTest.executeSoon(finishTest); +} + +function finishTest() { + debugDump("entering"); + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_LOG)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_LOG); + } + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> +</body> +</window> diff --git a/toolkit/mozapps/update/tests/chrome/update.sjs b/toolkit/mozapps/update/tests/chrome/update.sjs new file mode 100644 index 000000000..78bb1b93f --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/update.sjs @@ -0,0 +1,194 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Server side http server script for application update tests. + */ + +const { classes: Cc, interfaces: Ci } = Components; + +const REL_PATH_DATA = "chrome/toolkit/mozapps/update/tests/data/"; + +function getTestDataFile(aFilename) { + let file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties).get("CurWorkD", Ci.nsILocalFile); + let pathParts = REL_PATH_DATA.split("/"); + for (let i = 0; i < pathParts.length; ++i) { + file.append(pathParts[i]); + } + if (aFilename) { + file.append(aFilename); + } + return file; +} + +function loadHelperScript() { + let scriptFile = getTestDataFile("sharedUpdateXML.js"); + let io = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService2); + let scriptSpec = io.newFileURI(scriptFile).spec; + let scriptloader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + scriptloader.loadSubScript(scriptSpec, this); +} +loadHelperScript(); + +const URL_HOST = "http://example.com"; +const URL_PATH_UPDATE_XML = "/chrome/toolkit/mozapps/update/tests/chrome/update.sjs"; +const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML; +const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR; + +const SLOW_MAR_DOWNLOAD_INTERVAL = 100; +var gTimer; + +function handleRequest(aRequest, aResponse) { + let params = { }; + if (aRequest.queryString) { + params = parseQueryString(aRequest.queryString); + } + + let statusCode = params.statusCode ? parseInt(params.statusCode) : 200; + let statusReason = params.statusReason ? params.statusReason : "OK"; + aResponse.setStatusLine(aRequest.httpVersion, statusCode, statusReason); + aResponse.setHeader("Cache-Control", "no-cache", false); + + // When a mar download is started by the update service it can finish + // downloading before the ui has loaded. By specifying a serviceURL for the + // update patch that points to this file and has a slowDownloadMar param the + // mar will be downloaded asynchronously which will allow the ui to load + // before the download completes. + if (params.slowDownloadMar) { + aResponse.processAsync(); + aResponse.setHeader("Content-Type", "binary/octet-stream"); + aResponse.setHeader("Content-Length", SIZE_SIMPLE_MAR); + var continueFile = getTestDataFile("continue"); + var contents = readFileBytes(getTestDataFile(FILE_SIMPLE_MAR)); + gTimer = Cc["@mozilla.org/timer;1"]. + createInstance(Ci.nsITimer); + gTimer.initWithCallback(function(aTimer) { + if (continueFile.exists()) { + gTimer.cancel(); + aResponse.write(contents); + aResponse.finish(); + } + }, SLOW_MAR_DOWNLOAD_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK); + return; + } + + if (params.uiURL) { + let remoteType = ""; + if (!params.remoteNoTypeAttr && params.uiURL == "BILLBOARD") { + remoteType = " " + params.uiURL.toLowerCase() + "=\"1\""; + } + aResponse.write("<html><head><meta http-equiv=\"content-type\" content=" + + "\"text/html; charset=utf-8\"></head><body" + + remoteType + ">" + params.uiURL + + "<br><br>this is a test mar that will not affect your " + + "build.</body></html>"); + return; + } + + if (params.xmlMalformed) { + aResponse.write("xml error"); + return; + } + + if (params.noUpdates) { + aResponse.write(getRemoteUpdatesXMLString("")); + return; + } + + if (params.unsupported) { + aResponse.write(getRemoteUpdatesXMLString(" <update type=\"major\" " + + "unsupported=\"true\" " + + "detailsURL=\"" + URL_HOST + + "\"></update>\n")); + return; + } + + let size; + let patches = ""; + if (!params.partialPatchOnly) { + size = SIZE_SIMPLE_MAR + (params.invalidCompleteSize ? "1" : ""); + patches += getRemotePatchString("complete", SERVICE_URL, "SHA512", + SHA512_HASH_SIMPLE_MAR, size); + } + + if (!params.completePatchOnly) { + size = SIZE_SIMPLE_MAR + (params.invalidPartialSize ? "1" : ""); + patches += getRemotePatchString("partial", SERVICE_URL, "SHA512", + SHA512_HASH_SIMPLE_MAR, size); + } + + let type = params.type ? params.type : "major"; + let name = params.name ? params.name : "App Update Test"; + let appVersion = params.appVersion ? params.appVersion : "999999.9"; + let displayVersion = params.displayVersion ? params.displayVersion + : "version " + appVersion; + let buildID = params.buildID ? params.buildID : "01234567890123"; + // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244 +// let detailsURL = params.showDetails ? URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS" : null; + let detailsURL = URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS"; + let showPrompt = params.showPrompt ? "true" : null; + let showNever = params.showNever ? "true" : null; + let promptWaitTime = params.promptWaitTime ? params.promptWaitTime : null; + + let updates = getRemoteUpdateString(patches, type, "App Update Test", + displayVersion, appVersion, buildID, + detailsURL, showPrompt, showNever, + promptWaitTime); + aResponse.write(getRemoteUpdatesXMLString(updates)); +} + +/** + * Helper function to create a JS object representing the url parameters from + * the request's queryString. + * + * @param aQueryString + * The request's query string. + * @return A JS object representing the url parameters from the request's + * queryString. + */ +function parseQueryString(aQueryString) { + let paramArray = aQueryString.split("&"); + let regex = /^([^=]+)=(.*)$/; + let params = {}; + for (let i = 0, sz = paramArray.length; i < sz; i++) { + let match = regex.exec(paramArray[i]); + if (!match) { + throw "Bad parameter in queryString! '" + paramArray[i] + "'"; + } + params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); + } + + return params; +} + +/** + * Reads the binary contents of a file and returns it as a string. + * + * @param aFile + * The file to read from. + * @return The contents of the file as a string. + */ +function readFileBytes(aFile) { + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fis.init(aFile, -1, -1, false); + let bis = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let data = []; + let count = fis.available(); + while (count > 0) { + let bytes = bis.readByteArray(Math.min(65535, count)); + data.push(String.fromCharCode.apply(null, bytes)); + count -= bytes.length; + if (bytes.length == 0) { + throw "Nothing read from input stream!"; + } + } + data.join(''); + fis.close(); + return data.toString(); +} diff --git a/toolkit/mozapps/update/tests/chrome/utils.js b/toolkit/mozapps/update/tests/chrome/utils.js new file mode 100644 index 000000000..31d0d2e5a --- /dev/null +++ b/toolkit/mozapps/update/tests/chrome/utils.js @@ -0,0 +1,1011 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test Definition + * + * Most tests can use an array named TESTS that will perform most if not all of + * the necessary checks. Each element in the array must be an object with the + * following possible properties. Additional properties besides the ones listed + * below can be added as needed. + * + * overrideCallback (optional) + * The function to call for the next test. This is typically called when the + * wizard page changes but can also be called for other events by the previous + * test. If this property isn't defined then the defaultCallback function will + * be called. If this property is defined then all other properties are + * optional. + * + * pageid (required unless overrideCallback is specified) + * The expected pageid for the wizard. This property is required unless the + * overrideCallback property is defined. + * + * extraStartFunction (optional) + * The function to call at the beginning of the defaultCallback function. If + * the function returns true the defaultCallback function will return early + * which allows waiting for a specific condition to be evaluated in the + * function specified in the extraStartFunction property before continuing + * with the test. + * + * extraCheckFunction (optional) + * The function to call to perform extra checks in the defaultCallback + * function. + * + * extraDelayedCheckFunction (optional) + * The function to call to perform extra checks in the delayedDefaultCallback + * function. + * + * buttonStates (optional) + * A javascript object representing the expected hidden and disabled attribute + * values for the buttons of the current wizard page. The values are checked + * in the delayedDefaultCallback function. For information about the structure + * of this object refer to the getExpectedButtonStates and checkButtonStates + * functions. + * + * buttonClick (optional) + * The current wizard page button to click at the end of the + * delayedDefaultCallback function. If the buttonClick property is defined + * then the extraDelayedFinishFunction property can't be specified due to race + * conditions in some of the tests and if both of them are specified the test + * will intentionally throw. + * + * extraDelayedFinishFunction (optional) + * The function to call at the end of the delayedDefaultCallback function. + * If the extraDelayedFinishFunction property is defined then the buttonClick + * property can't be specified due to race conditions in some of the tests and + * if both of them are specified the test will intentionally throw. + * + * ranTest (should not be specified) + * When delayedDefaultCallback is called a property named ranTest is added to + * the current test so it is possible to verify that each test in the TESTS + * array has ran. + * + * prefHasUserValue (optional) + * For comparing the expected value defined by this property with the return + * value of prefHasUserValue using gPrefToCheck for the preference name in the + * checkPrefHasUserValue function. + */ + +'use strict'; + +/* globals TESTS, runTest, finishTest */ + +const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr, + utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm", this); + +const IS_MACOSX = ("nsILocalFileMac" in Ci); +const IS_WIN = ("@mozilla.org/windows-registry-key;1" in Cc); + +// The tests have to use the pageid instead of the pageIndex due to the +// app update wizard's access method being random. +const PAGEID_DUMMY = "dummy"; // Done +const PAGEID_CHECKING = "checking"; // Done +const PAGEID_NO_UPDATES_FOUND = "noupdatesfound"; // Done +const PAGEID_MANUAL_UPDATE = "manualUpdate"; // Done +const PAGEID_UNSUPPORTED = "unsupported"; // Done +const PAGEID_FOUND_BASIC = "updatesfoundbasic"; // Done +const PAGEID_DOWNLOADING = "downloading"; // Done +const PAGEID_ERRORS = "errors"; // Done +const PAGEID_ERROR_EXTRA = "errorextra"; // Done +const PAGEID_ERROR_PATCHING = "errorpatching"; // Done +const PAGEID_FINISHED = "finished"; // Done +const PAGEID_FINISHED_BKGRD = "finishedBackground"; // Done + +const UPDATE_WINDOW_NAME = "Update:Wizard"; + +const URL_HOST = "http://example.com"; +const URL_PATH_UPDATE_XML = "/chrome/toolkit/mozapps/update/tests/chrome/update.sjs"; +const REL_PATH_DATA = "chrome/toolkit/mozapps/update/tests/data"; + +// These two URLs must not contain parameters since tests add their own +// test specific parameters. +const URL_HTTP_UPDATE_XML = URL_HOST + URL_PATH_UPDATE_XML; +const URL_HTTPS_UPDATE_XML = "https://example.com" + URL_PATH_UPDATE_XML; + +const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul"; + +const PREF_APP_UPDATE_INTERVAL = "app.update.interval"; +const PREF_APP_UPDATE_LASTUPDATETIME = "app.update.lastUpdateTime.background-update-timer"; + +const LOG_FUNCTION = info; + +const BIN_SUFFIX = (IS_WIN ? ".exe" : ""); +const FILE_UPDATER_BIN = "updater" + (IS_MACOSX ? ".app" : BIN_SUFFIX); +const FILE_UPDATER_BIN_BAK = FILE_UPDATER_BIN + ".bak"; + +var gURLData = URL_HOST + "/" + REL_PATH_DATA + "/"; + +var gTestTimeout = 240000; // 4 minutes +var gTimeoutTimer; + +// The number of SimpleTest.executeSoon calls to perform when waiting on an +// update window to close before giving up. +const CLOSE_WINDOW_TIMEOUT_MAXCOUNT = 10; +// Counter for the SimpleTest.executeSoon when waiting on an update window to +// close before giving up. +var gCloseWindowTimeoutCounter = 0; + +// The following vars are for restoring previous preference values (if present) +// when the test finishes. +var gAppUpdateEnabled; // app.update.enabled +var gAppUpdateServiceEnabled; // app.update.service.enabled +var gAppUpdateStagingEnabled; // app.update.staging.enabled +var gAppUpdateURLDefault; // app.update.url (default prefbranch) + +var gTestCounter = -1; +var gWin; +var gDocElem; +var gPrefToCheck; +var gUseTestUpdater = false; + +// Set to true to log additional information for debugging. To log additional +// information for an individual test set DEBUG_AUS_TEST to true in the test's +// onload function. +var DEBUG_AUS_TEST = true; + +const DATA_URI_SPEC = "chrome://mochitests/content/chrome/toolkit/mozapps/update/tests/data/"; +/* import-globals-from ../data/shared.js */ +Services.scriptloader.loadSubScript(DATA_URI_SPEC + "shared.js", this); + +/** + * The current test in TESTS array. + */ +this.__defineGetter__("gTest", function() { + return TESTS[gTestCounter]; +}); + +/** + * The current test's callback. This will either return the callback defined in + * the test's overrideCallback property or defaultCallback if the + * overrideCallback property is undefined. + */ +this.__defineGetter__("gCallback", function() { + return gTest.overrideCallback ? gTest.overrideCallback + : defaultCallback; +}); + +/** + * nsIObserver for receiving window open and close notifications. + */ +const gWindowObserver = { + observe: function WO_observe(aSubject, aTopic, aData) { + let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + + if (aTopic == "domwindowclosed") { + if (win.location != URI_UPDATE_PROMPT_DIALOG) { + debugDump("domwindowclosed event for window not being tested - " + + "location: " + win.location + "... returning early"); + return; + } + // Allow tests the ability to provide their own function (it must be + // named finishTest) for finishing the test. + try { + finishTest(); + } + catch (e) { + finishTestDefault(); + } + return; + } + + win.addEventListener("load", function WO_observe_onLoad() { + win.removeEventListener("load", WO_observe_onLoad, false); + // Ignore windows other than the update UI window. + if (win.location != URI_UPDATE_PROMPT_DIALOG) { + debugDump("load event for window not being tested - location: " + + win.location + "... returning early"); + return; + } + + // The first wizard page should always be the dummy page. + let pageid = win.document.documentElement.currentPage.pageid; + if (pageid != PAGEID_DUMMY) { + // This should never happen but if it does this will provide a clue + // for diagnosing the cause. + ok(false, "Unexpected load event - pageid got: " + pageid + + ", expected: " + PAGEID_DUMMY + "... returning early"); + return; + } + + gWin = win; + gDocElem = gWin.document.documentElement; + gDocElem.addEventListener("pageshow", onPageShowDefault, false); + }, false); + } +}; + +/** + * Default test run function that can be used by most tests. This function uses + * protective measures to prevent the test from failing provided by + * |runTestDefaultWaitForWindowClosed| helper functions to prevent failure due + * to a previous test failure. + */ +function runTestDefault() { + debugDump("entering"); + + if (!("@mozilla.org/zipwriter;1" in Cc)) { + ok(false, "nsIZipWriter is required to run these tests"); + return; + } + + SimpleTest.waitForExplicitFinish(); + + runTestDefaultWaitForWindowClosed(); +} + +/** + * If an update window is found SimpleTest.executeSoon can callback before the + * update window is fully closed especially with debug builds. If an update + * window is found this function will call itself using SimpleTest.executeSoon + * up to the amount declared in CLOSE_WINDOW_TIMEOUT_MAXCOUNT until the update + * window has closed before continuing the test. + */ +function runTestDefaultWaitForWindowClosed() { + gCloseWindowTimeoutCounter++; + if (gCloseWindowTimeoutCounter > CLOSE_WINDOW_TIMEOUT_MAXCOUNT) { + try { + finishTest(); + } + catch (e) { + finishTestDefault(); + } + return; + } + + // The update window should not be open at this time. If it is the call to + // |closeUpdateWindow| will close it and cause the test to fail. + if (closeUpdateWindow()) { + SimpleTest.executeSoon(runTestDefaultWaitForWindowClosed); + } else { + Services.ww.registerNotification(gWindowObserver); + + gCloseWindowTimeoutCounter = 0; + + setupFiles(); + setupPrefs(); + gEnv.set("MOZ_TEST_SKIP_UPDATE_STAGE", "1"); + removeUpdateDirsAndFiles(); + reloadUpdateManagerData(); + setupTimer(gTestTimeout); + SimpleTest.executeSoon(setupTestUpdater); + } +} + +/** + * Default test finish function that can be used by most tests. This function + * uses protective measures to prevent the next test from failing provided by + * |finishTestDefaultWaitForWindowClosed| helper functions to prevent failure + * due to an update window being left open. + */ +function finishTestDefault() { + debugDump("entering"); + if (gTimeoutTimer) { + gTimeoutTimer.cancel(); + gTimeoutTimer = null; + } + + if (gChannel) { + debugDump("channel = " + gChannel); + gChannel = null; + gPrefRoot.removeObserver(PREF_APP_UPDATE_CHANNEL, observer); + } + + verifyTestsRan(); + + resetPrefs(); + gEnv.set("MOZ_TEST_SKIP_UPDATE_STAGE", ""); + resetFiles(); + removeUpdateDirsAndFiles(); + reloadUpdateManagerData(); + + Services.ww.unregisterNotification(gWindowObserver); + if (gDocElem) { + gDocElem.removeEventListener("pageshow", onPageShowDefault, false); + } + + finishTestRestoreUpdaterBackup(); +} + +/** + * nsITimerCallback for the timeout timer to cleanly finish a test if the Update + * Window doesn't close for a test. This allows the next test to run properly if + * a previous test fails. + * + * @param aTimer + * The nsITimer that fired. + */ +function finishTestTimeout(aTimer) { + ok(false, "Test timed out. Maximum time allowed is " + (gTestTimeout / 1000) + + " seconds"); + + try { + finishTest(); + } + catch (e) { + finishTestDefault(); + } +} + +/** + * When a test finishes this will repeatedly attempt to restore the real updater + * for tests that use the test updater and then call + * finishTestDefaultWaitForWindowClosed after the restore is successful. + */ +function finishTestRestoreUpdaterBackup() { + if (gUseTestUpdater) { + try { + // Windows debug builds keep the updater file in use for a short period of + // time after the updater process exits. + restoreUpdaterBackup(); + } catch (e) { + logTestInfo("Attempt to restore the backed up updater failed... " + + "will try again, Exception: " + e); + SimpleTest.executeSoon(finishTestRestoreUpdaterBackup); + return; + } + } + + finishTestDefaultWaitForWindowClosed(); +} + +/** + * If an update window is found SimpleTest.executeSoon can callback before the + * update window is fully closed especially with debug builds. If an update + * window is found this function will call itself using SimpleTest.executeSoon + * up to the amount declared in CLOSE_WINDOW_TIMEOUT_MAXCOUNT until the update + * window has closed before finishing the test. + */ +function finishTestDefaultWaitForWindowClosed() { + gCloseWindowTimeoutCounter++; + if (gCloseWindowTimeoutCounter > CLOSE_WINDOW_TIMEOUT_MAXCOUNT) { + SimpleTest.requestCompleteLog(); + SimpleTest.finish(); + return; + } + + // The update window should not be open at this time. If it is the call to + // |closeUpdateWindow| will close it and cause the test to fail. + if (closeUpdateWindow()) { + SimpleTest.executeSoon(finishTestDefaultWaitForWindowClosed); + } else { + SimpleTest.finish(); + } +} + +/** + * Default callback for the wizard's documentElement pageshow listener. This + * will return early for event's where the originalTarget's nodeName is not + * wizardpage. + */ +function onPageShowDefault(aEvent) { + if (!gTimeoutTimer) { + debugDump("gTimeoutTimer is null... returning early"); + return; + } + + // Return early if the event's original target isn't for a wizardpage element. + // This check is necessary due to the remotecontent element firing pageshow. + if (aEvent.originalTarget.nodeName != "wizardpage") { + debugDump("only handles events with an originalTarget nodeName of " + + "|wizardpage|. aEvent.originalTarget.nodeName = " + + aEvent.originalTarget.nodeName + "... returning early"); + return; + } + + gTestCounter++; + gCallback(aEvent); +} + +/** + * Default callback that can be used by most tests. + */ +function defaultCallback(aEvent) { + if (!gTimeoutTimer) { + debugDump("gTimeoutTimer is null... returning early"); + return; + } + + debugDump("entering - TESTS[" + gTestCounter + "], pageid: " + gTest.pageid + + ", aEvent.originalTarget.nodeName: " + + aEvent.originalTarget.nodeName); + + if (gTest && gTest.extraStartFunction) { + debugDump("calling extraStartFunction " + gTest.extraStartFunction.name); + if (gTest.extraStartFunction(aEvent)) { + debugDump("extraStartFunction early return"); + return; + } + } + + is(gDocElem.currentPage.pageid, gTest.pageid, + "Checking currentPage.pageid equals " + gTest.pageid + " in pageshow"); + + // Perform extra checks if specified by the test + if (gTest.extraCheckFunction) { + debugDump("calling extraCheckFunction " + gTest.extraCheckFunction.name); + gTest.extraCheckFunction(); + } + + // The wizard page buttons' disabled and hidden attributes are set after the + // pageshow event so use executeSoon to allow them to be set so their disabled + // and hidden attribute values can be checked. + SimpleTest.executeSoon(delayedDefaultCallback); +} + +/** + * Delayed default callback called using executeSoon in defaultCallback which + * allows the wizard page buttons' disabled and hidden attributes to be set + * before checking their values. + */ +function delayedDefaultCallback() { + if (!gTimeoutTimer) { + debugDump("gTimeoutTimer is null... returning early"); + return; + } + + if (!gTest) { + debugDump("gTest is null... returning early"); + return; + } + + debugDump("entering - TESTS[" + gTestCounter + "], pageid: " + gTest.pageid); + + // Verify the pageid hasn't changed after executeSoon was called. + is(gDocElem.currentPage.pageid, gTest.pageid, + "Checking currentPage.pageid equals " + gTest.pageid + " after " + + "executeSoon"); + + checkButtonStates(); + + // Perform delayed extra checks if specified by the test + if (gTest.extraDelayedCheckFunction) { + debugDump("calling extraDelayedCheckFunction " + + gTest.extraDelayedCheckFunction.name); + gTest.extraDelayedCheckFunction(); + } + + // Used to verify that this test has been performed + gTest.ranTest = true; + + if (gTest.buttonClick) { + debugDump("clicking " + gTest.buttonClick + " button"); + if (gTest.extraDelayedFinishFunction) { + throw ("Tests cannot have a buttonClick and an extraDelayedFinishFunction property"); + } + gDocElem.getButton(gTest.buttonClick).click(); + } else if (gTest.extraDelayedFinishFunction) { + debugDump("calling extraDelayedFinishFunction " + + gTest.extraDelayedFinishFunction.name); + gTest.extraDelayedFinishFunction(); + } +} + +/** + * Gets the continue file used to signal the mock http server to continue + * downloading for slow download mar file tests without creating it. + * + * @return nsILocalFile for the continue file. + */ +function getContinueFile() { + let continueFile = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("CurWorkD", Ci.nsILocalFile); + let continuePath = REL_PATH_DATA + "/continue"; + let continuePathParts = continuePath.split("/"); + for (let i = 0; i < continuePathParts.length; ++i) { + continueFile.append(continuePathParts[i]); + } + return continueFile; +} + +/** + * Creates the continue file used to signal the mock http server to continue + * downloading for slow download mar file tests. + */ +function createContinueFile() { + debugDump("creating 'continue' file for slow mar downloads"); + writeFile(getContinueFile(), ""); +} + +/** + * Removes the continue file used to signal the mock http server to continue + * downloading for slow download mar file tests. + */ +function removeContinueFile() { + let continueFile = getContinueFile(); + if (continueFile.exists()) { + debugDump("removing 'continue' file for slow mar downloads"); + continueFile.remove(false); + } +} + +/** + * Checks the wizard page buttons' disabled and hidden attributes values are + * correct. If an expected button id is not specified then the expected disabled + * and hidden attribute value is true. + */ +function checkButtonStates() { + debugDump("entering - TESTS[" + gTestCounter + "], pageid: " + gTest.pageid); + + const buttonNames = ["extra1", "extra2", "back", "next", "finish", "cancel"]; + let buttonStates = getExpectedButtonStates(); + buttonNames.forEach(function(aButtonName) { + let button = gDocElem.getButton(aButtonName); + let hasHidden = aButtonName in buttonStates && + "hidden" in buttonStates[aButtonName]; + let hidden = hasHidden ? buttonStates[aButtonName].hidden : true; + let hasDisabled = aButtonName in buttonStates && + "disabled" in buttonStates[aButtonName]; + let disabled = hasDisabled ? buttonStates[aButtonName].disabled : true; + is(button.hidden, hidden, "Checking " + aButtonName + " button " + + "hidden attribute value equals " + (hidden ? "true" : "false")); + is(button.disabled, disabled, "Checking " + aButtonName + " button " + + "disabled attribute value equals " + (disabled ? "true" : "false")); + }); +} + +/** + * Returns the expected disabled and hidden attribute values for the buttons of + * the current wizard page. + */ +function getExpectedButtonStates() { + // Allow individual tests to override the expected button states. + if (gTest.buttonStates) { + return gTest.buttonStates; + } + + switch (gTest.pageid) { + case PAGEID_CHECKING: + return {cancel: {disabled: false, hidden: false}}; + case PAGEID_FOUND_BASIC: + if (gTest.neverButton) { + return {extra1: {disabled: false, hidden: false}, + extra2: {disabled: false, hidden: false}, + next: {disabled: false, hidden: false}}; + } + return {extra1: {disabled: false, hidden: false}, + next: {disabled: false, hidden: false}}; + case PAGEID_DOWNLOADING: + return {extra1: {disabled: false, hidden: false}}; + case PAGEID_NO_UPDATES_FOUND: + case PAGEID_MANUAL_UPDATE: + case PAGEID_UNSUPPORTED: + case PAGEID_ERRORS: + case PAGEID_ERROR_EXTRA: + return {finish: {disabled: false, hidden: false}}; + case PAGEID_ERROR_PATCHING: + return {next: { disabled: false, hidden: false}}; + case PAGEID_FINISHED: + case PAGEID_FINISHED_BKGRD: + return {extra1: { disabled: false, hidden: false}, + finish: { disabled: false, hidden: false}}; + } + return null; +} + +/** + * Compares the return value of prefHasUserValue for the preference specified in + * gPrefToCheck with the value passed in the aPrefHasValue parameter or the + * value specified in the current test's prefHasUserValue property if + * aPrefHasValue is undefined. + * + * @param aPrefHasValue (optional) + * The expected value returned from prefHasUserValue for the preference + * specified in gPrefToCheck. If aPrefHasValue is undefined the value + * of the current test's prefHasUserValue property will be used. + */ +function checkPrefHasUserValue(aPrefHasValue) { + let prefHasUserValue = aPrefHasValue === undefined ? gTest.prefHasUserValue + : aPrefHasValue; + is(Services.prefs.prefHasUserValue(gPrefToCheck), prefHasUserValue, + "Checking prefHasUserValue for preference " + gPrefToCheck + " equals " + + (prefHasUserValue ? "true" : "false")); +} + +/** + * Checks whether the link is hidden for a general background update check error + * or not on the errorextra page and that the app.update.backgroundErrors + * preference does not have a user value. + * + * @param aShouldBeHidden (optional) + * The expected value for the label's hidden attribute for the link. If + * aShouldBeHidden is undefined the value of the current test's + * shouldBeHidden property will be used. + */ +function checkErrorExtraPage(aShouldBeHidden) { + let shouldBeHidden = aShouldBeHidden === undefined ? gTest.shouldBeHidden + : aShouldBeHidden; + is(gWin.document.getElementById("errorExtraLinkLabel").hidden, shouldBeHidden, + "Checking errorExtraLinkLabel hidden attribute equals " + + (shouldBeHidden ? "true" : "false")); + + is(gWin.document.getElementById(gTest.displayedTextElem).hidden, false, + "Checking " + gTest.displayedTextElem + " should not be hidden"); + + ok(!Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS), + "Preference " + PREF_APP_UPDATE_BACKGROUNDERRORS + " should not have a " + + "user value"); +} + +/** + * Gets the update version info for the update url parameters to send to + * update.sjs. + * + * @param aAppVersion (optional) + * The application version for the update snippet. If not specified the + * current application version will be used. + * @return The url parameters for the application and platform version to send + * to update.sjs. + */ +function getVersionParams(aAppVersion) { + let appInfo = Services.appinfo; + return "&appVersion=" + (aAppVersion ? aAppVersion : appInfo.version); +} + +/** + * Verifies that all tests ran. + */ +function verifyTestsRan() { + debugDump("entering"); + + // Return early if there are no tests defined. + if (!TESTS) { + return; + } + + gTestCounter = -1; + for (let i = 0; i < TESTS.length; ++i) { + gTestCounter++; + let test = TESTS[i]; + let msg = "Checking if TESTS[" + i + "] test was performed... " + + "callback function name = " + gCallback.name + ", " + + "pageid = " + test.pageid; + ok(test.ranTest, msg); + } +} + +/** + * Creates a backup of files the tests need to modify so they can be restored to + * the original file when the test has finished and then modifies the files. + */ +function setupFiles() { + // Backup the updater-settings.ini file if it exists by moving it. + let baseAppDir = getGREDir(); + let updateSettingsIni = baseAppDir.clone(); + updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); + if (updateSettingsIni.exists()) { + updateSettingsIni.moveTo(baseAppDir, FILE_UPDATE_SETTINGS_INI_BAK); + } + updateSettingsIni = baseAppDir.clone(); + updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); + writeFile(updateSettingsIni, UPDATE_SETTINGS_CONTENTS); +} + +/** + * For tests that use the test updater restores the backed up real updater if + * it exists and tries again on failure since Windows debug builds at times + * leave the file in use. After success moveRealUpdater is called to continue + * the setup of the test updater. For tests that don't use the test updater + * runTest will be called. + */ +function setupTestUpdater() { + if (!gUseTestUpdater) { + runTest(); + return; + } + + try { + restoreUpdaterBackup(); + } catch (e) { + logTestInfo("Attempt to restore the backed up updater failed... " + + "will try again, Exception: " + e); + SimpleTest.executeSoon(setupTestUpdater); + return; + } + moveRealUpdater(); +} + +/** + * Backs up the real updater and tries again on failure since Windows debug + * builds at times leave the file in use. After success it will call + * copyTestUpdater to continue the setup of the test updater. + */ +function moveRealUpdater() { + try { + // Move away the real updater + let baseAppDir = getAppBaseDir(); + let updater = baseAppDir.clone(); + updater.append(FILE_UPDATER_BIN); + updater.moveTo(baseAppDir, FILE_UPDATER_BIN_BAK); + } catch (e) { + logTestInfo("Attempt to move the real updater out of the way failed... " + + "will try again, Exception: " + e); + SimpleTest.executeSoon(moveRealUpdater); + return; + } + + copyTestUpdater(); +} + +/** + * Copies the test updater so it can be used by tests and tries again on failure + * since Windows debug builds at times leave the file in use. After success it + * will call runTest to continue the test. + */ +function copyTestUpdater() { + try { + // Copy the test updater + let baseAppDir = getAppBaseDir(); + let testUpdaterDir = Services.dirsvc.get("CurWorkD", Ci.nsILocalFile); + let relPath = REL_PATH_DATA; + let pathParts = relPath.split("/"); + for (let i = 0; i < pathParts.length; ++i) { + testUpdaterDir.append(pathParts[i]); + } + + let testUpdater = testUpdaterDir.clone(); + testUpdater.append(FILE_UPDATER_BIN); + testUpdater.copyToFollowingLinks(baseAppDir, FILE_UPDATER_BIN); + } catch (e) { + logTestInfo("Attempt to copy the test updater failed... " + + "will try again, Exception: " + e); + SimpleTest.executeSoon(copyTestUpdater); + return; + } + + runTest(); +} + +/** + * Restores the updater that was backed up. This is called in setupTestUpdater + * before the backup of the real updater is done in case the previous test + * failed to restore the updater, in finishTestDefaultWaitForWindowClosed when + * the test has finished, and in test_9999_cleanup.xul after all tests have + * finished. + */ +function restoreUpdaterBackup() { + let baseAppDir = getAppBaseDir(); + let updater = baseAppDir.clone(); + let updaterBackup = baseAppDir.clone(); + updater.append(FILE_UPDATER_BIN); + updaterBackup.append(FILE_UPDATER_BIN_BAK); + if (updaterBackup.exists()) { + if (updater.exists()) { + updater.remove(true); + } + updaterBackup.moveTo(baseAppDir, FILE_UPDATER_BIN); + } +} + +/** + * Sets the most common preferences used by tests to values used by the majority + * of the tests and when necessary saves the preference's original values if + * present so they can be set back to the original values when the test has + * finished. + */ +function setupPrefs() { + if (DEBUG_AUS_TEST) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, true); + } + + // Prevent nsIUpdateTimerManager from notifying nsIApplicationUpdateService + // to check for updates by setting the app update last update time to the + // current time minus one minute in seconds and the interval time to 12 hours + // in seconds. + let now = Math.round(Date.now() / 1000) - 60; + Services.prefs.setIntPref(PREF_APP_UPDATE_LASTUPDATETIME, now); + Services.prefs.setIntPref(PREF_APP_UPDATE_INTERVAL, 43200); + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ENABLED)) { + gAppUpdateEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_ENABLED); + } + Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, true); + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ENABLED)) { + gAppUpdateServiceEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED); + } + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false); + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_STAGING_ENABLED)) { + gAppUpdateStagingEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED); + } + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false); + + Services.prefs.setIntPref(PREF_APP_UPDATE_IDLETIME, 0); + Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 0); + Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false); + Services.prefs.setIntPref(PREF_APP_UPDATE_DOWNLOADBACKGROUNDINTERVAL, 0); +} + +/** + * Restores files that were backed up for the tests and general file cleanup. + */ +function resetFiles() { + // Restore the backed up updater-settings.ini if it exists. + let baseAppDir = getGREDir(); + let updateSettingsIni = baseAppDir.clone(); + updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI_BAK); + if (updateSettingsIni.exists()) { + updateSettingsIni.moveTo(baseAppDir, FILE_UPDATE_SETTINGS_INI); + } + + // Not being able to remove the "updated" directory will not adversely affect + // subsequent tests so wrap it in a try block and don't test whether its + // removal was successful. + let updatedDir; + if (IS_MACOSX) { + updatedDir = getUpdatesDir(); + updatedDir.append(DIR_PATCH); + } else { + updatedDir = getAppBaseDir(); + } + updatedDir.append(DIR_UPDATED); + if (updatedDir.exists()) { + try { + removeDirRecursive(updatedDir); + } + catch (e) { + logTestInfo("Unable to remove directory. Path: " + updatedDir.path + + ", Exception: " + e); + } + } +} + +/** + * Resets the most common preferences used by tests to their original values. + */ +function resetPrefs() { + if (gAppUpdateURLDefault) { + gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_URL, gAppUpdateURLDefault); + } + + if (gAppUpdateEnabled !== undefined) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, gAppUpdateEnabled); + } else if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ENABLED)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ENABLED); + } + + if (gAppUpdateServiceEnabled !== undefined) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, gAppUpdateServiceEnabled); + } else if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ENABLED)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ENABLED); + } + + if (gAppUpdateStagingEnabled !== undefined) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, gAppUpdateStagingEnabled); + } else if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_STAGING_ENABLED)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_STAGING_ENABLED); + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_IDLETIME)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_IDLETIME); + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_PROMPTWAITTIME)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_PROMPTWAITTIME); + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_URL_DETAILS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_URL_DETAILS); + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED); + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_LOG)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_LOG); + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SILENT)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_SILENT); + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS); + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDMAXERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDMAXERRORS); + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_DOWNLOADBACKGROUNDINTERVAL)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_DOWNLOADBACKGROUNDINTERVAL); + } + + try { + Services.prefs.deleteBranch(PREFBRANCH_APP_UPDATE_NEVER); + } + catch (e) { + } +} + +function setupTimer(aTestTimeout) { + gTestTimeout = aTestTimeout; + if (gTimeoutTimer) { + gTimeoutTimer.cancel(); + gTimeoutTimer = null; + } + gTimeoutTimer = Cc["@mozilla.org/timer;1"]. + createInstance(Ci.nsITimer); + gTimeoutTimer.initWithCallback(finishTestTimeout, gTestTimeout, + Ci.nsITimer.TYPE_ONE_SHOT); +} + +/** + * Closes the update window if it is open and causes the test to fail if an + * update window is found. + * + * @return true if an update window was found, otherwise false. + */ +function closeUpdateWindow() { + let updateWindow = getUpdateWindow(); + if (!updateWindow) { + return false; + } + + ok(false, "Found an existing Update Window from the current or a previous " + + "test... attempting to close it."); + updateWindow.close(); + return true; +} + +/** + * Gets the update window. + * + * @return The nsIDOMWindow for the Update Window if it is open and null + * if it isn't. + */ +function getUpdateWindow() { + return Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME); +} + +/** + * Helper for background check errors. + */ +const errorsPrefObserver = { + observedPref: null, + maxErrorPref: null, + + /** + * Sets up a preference observer and sets the associated maximum errors + * preference used for background notification. + * + * @param aObservePref + * The preference to observe. + * @param aMaxErrorPref + * The maximum errors preference. + * @param aMaxErrorCount + * The value to set the maximum errors preference to. + */ + init: function(aObservePref, aMaxErrorPref, aMaxErrorCount) { + this.observedPref = aObservePref; + this.maxErrorPref = aMaxErrorPref; + + let maxErrors = aMaxErrorCount ? aMaxErrorCount : 2; + Services.prefs.setIntPref(aMaxErrorPref, maxErrors); + Services.prefs.addObserver(aObservePref, this, false); + }, + + /** + * Preference observer for the preference specified in |this.observedPref|. + */ + observe: function XPI_observe(aSubject, aTopic, aData) { + if (aData == this.observedPref) { + let errCount = Services.prefs.getIntPref(this.observedPref); + let errMax = Services.prefs.getIntPref(this.maxErrorPref); + if (errCount >= errMax) { + debugDump("removing pref observer"); + Services.prefs.removeObserver(this.observedPref, this); + } else { + debugDump("notifying AUS"); + SimpleTest.executeSoon(function() { + gAUS.notify(null); + }); + } + } + } +}; diff --git a/toolkit/mozapps/update/tests/data/complete.exe b/toolkit/mozapps/update/tests/data/complete.exe Binary files differnew file mode 100644 index 000000000..da9cdf0cc --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete.exe diff --git a/toolkit/mozapps/update/tests/data/complete.mar b/toolkit/mozapps/update/tests/data/complete.mar Binary files differnew file mode 100644 index 000000000..52306e0a2 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete.mar diff --git a/toolkit/mozapps/update/tests/data/complete.png b/toolkit/mozapps/update/tests/data/complete.png Binary files differnew file mode 100644 index 000000000..2990a539f --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete.png diff --git a/toolkit/mozapps/update/tests/data/complete_log_success_mac b/toolkit/mozapps/update/tests/data/complete_log_success_mac new file mode 100644 index 000000000..4f992a137 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_log_success_mac @@ -0,0 +1,332 @@ +UPDATE TYPE complete +PREPARE REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0 +PREPARE REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png +PREPARE REMOVEFILE Contents/Resources/removed-files +PREPARE REMOVEFILE Contents/Resources/precomplete +PREPARE REMOVEFILE Contents/Resources/2/20/20text0 +PREPARE REMOVEFILE Contents/Resources/2/20/20png0.png +PREPARE REMOVEFILE Contents/Resources/0/0exe0.exe +PREPARE REMOVEFILE Contents/Resources/0/00/00text0 +PREPARE REMOVEFILE Contents/MacOS/exe0.exe +PREPARE REMOVEDIR Contents/Resources/searchplugins/ +PREPARE REMOVEDIR Contents/Resources/defaults/pref/ +PREPARE REMOVEDIR Contents/Resources/defaults/ +PREPARE REMOVEDIR Contents/Resources/2/20/ +PREPARE REMOVEDIR Contents/Resources/2/ +PREPARE REMOVEDIR Contents/Resources/0/00/ +PREPARE REMOVEDIR Contents/Resources/0/ +PREPARE REMOVEDIR Contents/Resources/ +PREPARE REMOVEDIR Contents/MacOS/ +PREPARE REMOVEDIR Contents/ +PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0 +PREPARE ADD Contents/Resources/searchplugins/searchpluginspng1.png +PREPARE ADD Contents/Resources/searchplugins/searchpluginspng0.png +PREPARE ADD Contents/Resources/removed-files +PREPARE ADD Contents/Resources/precomplete +PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +PREPARE ADD Contents/Resources/1/10/10text0 +PREPARE ADD Contents/Resources/0/0exe0.exe +PREPARE ADD Contents/Resources/0/00/00text1 +PREPARE ADD Contents/Resources/0/00/00text0 +PREPARE ADD Contents/Resources/0/00/00png0.png +PREPARE ADD Contents/MacOS/exe0.exe +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/98/ +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/970/ +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/971/ +PREPARE REMOVEDIR Contents/Resources/9/97/ +PREPARE REMOVEFILE Contents/Resources/9/96/96text0 +PREPARE REMOVEFILE Contents/Resources/9/96/96text1 +PREPARE REMOVEDIR Contents/Resources/9/96/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/93/ +PREPARE REMOVEDIR Contents/Resources/9/92/ +PREPARE REMOVEDIR Contents/Resources/9/91/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/88/ +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/870/ +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/871/ +PREPARE REMOVEDIR Contents/Resources/8/87/ +PREPARE REMOVEFILE Contents/Resources/8/86/86text0 +PREPARE REMOVEFILE Contents/Resources/8/86/86text1 +PREPARE REMOVEDIR Contents/Resources/8/86/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/83/ +PREPARE REMOVEDIR Contents/Resources/8/82/ +PREPARE REMOVEDIR Contents/Resources/8/81/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/70/ +PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/71/ +PREPARE REMOVEFILE Contents/Resources/7/7text0 +PREPARE REMOVEFILE Contents/Resources/7/7text1 +PREPARE REMOVEDIR Contents/Resources/7/ +PREPARE REMOVEDIR Contents/Resources/6/ +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5test.exe +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEDIR Contents/Resources/5/ +PREPARE REMOVEFILE Contents/Resources/4/4text1 +PREPARE REMOVEFILE Contents/Resources/4/4text0 +PREPARE REMOVEDIR Contents/Resources/4/ +PREPARE REMOVEFILE Contents/Resources/3/3text1 +PREPARE REMOVEFILE Contents/Resources/3/3text0 +EXECUTE REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0 +EXECUTE REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png +EXECUTE REMOVEFILE Contents/Resources/removed-files +EXECUTE REMOVEFILE Contents/Resources/precomplete +EXECUTE REMOVEFILE Contents/Resources/2/20/20text0 +EXECUTE REMOVEFILE Contents/Resources/2/20/20png0.png +EXECUTE REMOVEFILE Contents/Resources/0/0exe0.exe +EXECUTE REMOVEFILE Contents/Resources/0/00/00text0 +EXECUTE REMOVEFILE Contents/MacOS/exe0.exe +EXECUTE REMOVEDIR Contents/Resources/searchplugins/ +EXECUTE REMOVEDIR Contents/Resources/defaults/pref/ +EXECUTE REMOVEDIR Contents/Resources/defaults/ +EXECUTE REMOVEDIR Contents/Resources/2/20/ +EXECUTE REMOVEDIR Contents/Resources/2/ +EXECUTE REMOVEDIR Contents/Resources/0/00/ +EXECUTE REMOVEDIR Contents/Resources/0/ +EXECUTE REMOVEDIR Contents/Resources/ +EXECUTE REMOVEDIR Contents/MacOS/ +EXECUTE REMOVEDIR Contents/ +EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0 +EXECUTE ADD Contents/Resources/searchplugins/searchpluginspng1.png +EXECUTE ADD Contents/Resources/searchplugins/searchpluginspng0.png +EXECUTE ADD Contents/Resources/removed-files +EXECUTE ADD Contents/Resources/precomplete +EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +EXECUTE ADD Contents/Resources/1/10/10text0 +EXECUTE ADD Contents/Resources/0/0exe0.exe +EXECUTE ADD Contents/Resources/0/00/00text1 +EXECUTE ADD Contents/Resources/0/00/00text0 +EXECUTE ADD Contents/Resources/0/00/00png0.png +EXECUTE ADD Contents/MacOS/exe0.exe +EXECUTE REMOVEDIR Contents/Resources/9/99/ +EXECUTE REMOVEDIR Contents/Resources/9/99/ +EXECUTE REMOVEDIR Contents/Resources/9/98/ +EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext0 +EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext1 +EXECUTE REMOVEDIR Contents/Resources/9/97/970/ +EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext0 +EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext1 +EXECUTE REMOVEDIR Contents/Resources/9/97/971/ +EXECUTE REMOVEDIR Contents/Resources/9/97/ +EXECUTE REMOVEFILE Contents/Resources/9/96/96text0 +EXECUTE REMOVEFILE Contents/Resources/9/96/96text1 +EXECUTE REMOVEDIR Contents/Resources/9/96/ +EXECUTE REMOVEDIR Contents/Resources/9/95/ +EXECUTE REMOVEDIR Contents/Resources/9/95/ +EXECUTE REMOVEDIR Contents/Resources/9/94/ +EXECUTE REMOVEDIR Contents/Resources/9/94/ +EXECUTE REMOVEDIR Contents/Resources/9/93/ +EXECUTE REMOVEDIR Contents/Resources/9/92/ +EXECUTE REMOVEDIR Contents/Resources/9/91/ +EXECUTE REMOVEDIR Contents/Resources/9/90/ +EXECUTE REMOVEDIR Contents/Resources/9/90/ +EXECUTE REMOVEDIR Contents/Resources/8/89/ +EXECUTE REMOVEDIR Contents/Resources/8/89/ +EXECUTE REMOVEDIR Contents/Resources/8/88/ +EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext0 +EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext1 +EXECUTE REMOVEDIR Contents/Resources/8/87/870/ +EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext0 +EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext1 +EXECUTE REMOVEDIR Contents/Resources/8/87/871/ +EXECUTE REMOVEDIR Contents/Resources/8/87/ +EXECUTE REMOVEFILE Contents/Resources/8/86/86text0 +EXECUTE REMOVEFILE Contents/Resources/8/86/86text1 +EXECUTE REMOVEDIR Contents/Resources/8/86/ +EXECUTE REMOVEDIR Contents/Resources/8/85/ +EXECUTE REMOVEDIR Contents/Resources/8/85/ +EXECUTE REMOVEDIR Contents/Resources/8/84/ +EXECUTE REMOVEDIR Contents/Resources/8/84/ +EXECUTE REMOVEDIR Contents/Resources/8/83/ +EXECUTE REMOVEDIR Contents/Resources/8/82/ +EXECUTE REMOVEDIR Contents/Resources/8/81/ +EXECUTE REMOVEDIR Contents/Resources/8/80/ +EXECUTE REMOVEDIR Contents/Resources/8/80/ +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtest.exe +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext0 +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext1 +EXECUTE REMOVEDIR Contents/Resources/7/70/ +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtest.exe +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext0 +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext1 +EXECUTE REMOVEDIR Contents/Resources/7/71/ +EXECUTE REMOVEFILE Contents/Resources/7/7text0 +EXECUTE REMOVEFILE Contents/Resources/7/7text1 +EXECUTE REMOVEDIR Contents/Resources/7/ +EXECUTE REMOVEDIR Contents/Resources/6/ +EXECUTE REMOVEFILE Contents/Resources/5/5text1 +EXECUTE REMOVEFILE Contents/Resources/5/5text0 +EXECUTE REMOVEFILE Contents/Resources/5/5test.exe +EXECUTE REMOVEFILE Contents/Resources/5/5text0 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEFILE Contents/Resources/5/5text1 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEDIR Contents/Resources/5/ +EXECUTE REMOVEFILE Contents/Resources/4/4text1 +EXECUTE REMOVEFILE Contents/Resources/4/4text0 +EXECUTE REMOVEDIR Contents/Resources/4/ +EXECUTE REMOVEFILE Contents/Resources/3/3text1 +EXECUTE REMOVEFILE Contents/Resources/3/3text0 +FINISH REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0 +FINISH REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png +FINISH REMOVEFILE Contents/Resources/removed-files +FINISH REMOVEFILE Contents/Resources/precomplete +FINISH REMOVEFILE Contents/Resources/2/20/20text0 +FINISH REMOVEFILE Contents/Resources/2/20/20png0.png +FINISH REMOVEFILE Contents/Resources/0/0exe0.exe +FINISH REMOVEFILE Contents/Resources/0/00/00text0 +FINISH REMOVEFILE Contents/MacOS/exe0.exe +FINISH REMOVEDIR Contents/Resources/searchplugins/ +removing directory: Contents/Resources/searchplugins/, rv: 0 +FINISH REMOVEDIR Contents/Resources/defaults/pref/ +removing directory: Contents/Resources/defaults/pref/, rv: 0 +FINISH REMOVEDIR Contents/Resources/defaults/ +removing directory: Contents/Resources/defaults/, rv: 0 +FINISH REMOVEDIR Contents/Resources/2/20/ +FINISH REMOVEDIR Contents/Resources/2/ +FINISH REMOVEDIR Contents/Resources/0/00/ +removing directory: Contents/Resources/0/00/, rv: 0 +FINISH REMOVEDIR Contents/Resources/0/ +removing directory: Contents/Resources/0/, rv: 0 +FINISH REMOVEDIR Contents/Resources/ +removing directory: Contents/Resources/, rv: 0 +FINISH REMOVEDIR Contents/MacOS/ +removing directory: Contents/MacOS/, rv: 0 +FINISH REMOVEDIR Contents/ +removing directory: Contents/, rv: 0 +FINISH ADD Contents/Resources/searchplugins/searchpluginstext0 +FINISH ADD Contents/Resources/searchplugins/searchpluginspng1.png +FINISH ADD Contents/Resources/searchplugins/searchpluginspng0.png +FINISH ADD Contents/Resources/removed-files +FINISH ADD Contents/Resources/precomplete +FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +FINISH ADD Contents/Resources/1/10/10text0 +FINISH ADD Contents/Resources/0/0exe0.exe +FINISH ADD Contents/Resources/0/00/00text1 +FINISH ADD Contents/Resources/0/00/00text0 +FINISH ADD Contents/Resources/0/00/00png0.png +FINISH ADD Contents/MacOS/exe0.exe +FINISH REMOVEDIR Contents/Resources/9/99/ +FINISH REMOVEDIR Contents/Resources/9/99/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/98/ +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0 +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1 +FINISH REMOVEDIR Contents/Resources/9/97/970/ +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0 +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1 +FINISH REMOVEDIR Contents/Resources/9/97/971/ +FINISH REMOVEDIR Contents/Resources/9/97/ +FINISH REMOVEFILE Contents/Resources/9/96/96text0 +FINISH REMOVEFILE Contents/Resources/9/96/96text1 +FINISH REMOVEDIR Contents/Resources/9/96/ +FINISH REMOVEDIR Contents/Resources/9/95/ +FINISH REMOVEDIR Contents/Resources/9/95/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/94/ +FINISH REMOVEDIR Contents/Resources/9/94/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/93/ +FINISH REMOVEDIR Contents/Resources/9/92/ +removing directory: Contents/Resources/9/92/, rv: 0 +FINISH REMOVEDIR Contents/Resources/9/91/ +removing directory: Contents/Resources/9/91/, rv: 0 +FINISH REMOVEDIR Contents/Resources/9/90/ +FINISH REMOVEDIR Contents/Resources/9/90/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/89/ +FINISH REMOVEDIR Contents/Resources/8/89/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/88/ +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0 +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1 +FINISH REMOVEDIR Contents/Resources/8/87/870/ +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0 +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1 +FINISH REMOVEDIR Contents/Resources/8/87/871/ +FINISH REMOVEDIR Contents/Resources/8/87/ +FINISH REMOVEFILE Contents/Resources/8/86/86text0 +FINISH REMOVEFILE Contents/Resources/8/86/86text1 +FINISH REMOVEDIR Contents/Resources/8/86/ +FINISH REMOVEDIR Contents/Resources/8/85/ +FINISH REMOVEDIR Contents/Resources/8/85/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/84/ +FINISH REMOVEDIR Contents/Resources/8/84/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/83/ +FINISH REMOVEDIR Contents/Resources/8/82/ +removing directory: Contents/Resources/8/82/, rv: 0 +FINISH REMOVEDIR Contents/Resources/8/81/ +removing directory: Contents/Resources/8/81/, rv: 0 +FINISH REMOVEDIR Contents/Resources/8/80/ +FINISH REMOVEDIR Contents/Resources/8/80/ +directory no longer exists; skipping +FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe +FINISH REMOVEFILE Contents/Resources/7/70/7xtext0 +FINISH REMOVEFILE Contents/Resources/7/70/7xtext1 +FINISH REMOVEDIR Contents/Resources/7/70/ +FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe +FINISH REMOVEFILE Contents/Resources/7/71/7xtext0 +FINISH REMOVEFILE Contents/Resources/7/71/7xtext1 +FINISH REMOVEDIR Contents/Resources/7/71/ +FINISH REMOVEFILE Contents/Resources/7/7text0 +FINISH REMOVEFILE Contents/Resources/7/7text1 +FINISH REMOVEDIR Contents/Resources/7/ +FINISH REMOVEDIR Contents/Resources/6/ +FINISH REMOVEFILE Contents/Resources/5/5text1 +FINISH REMOVEFILE Contents/Resources/5/5text0 +FINISH REMOVEFILE Contents/Resources/5/5test.exe +FINISH REMOVEDIR Contents/Resources/5/ +FINISH REMOVEFILE Contents/Resources/4/4text1 +FINISH REMOVEFILE Contents/Resources/4/4text0 +FINISH REMOVEDIR Contents/Resources/4/ +FINISH REMOVEFILE Contents/Resources/3/3text1 +FINISH REMOVEFILE Contents/Resources/3/3text0 +succeeded +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/complete_log_success_win b/toolkit/mozapps/update/tests/data/complete_log_success_win new file mode 100644 index 000000000..c5a03dc9d --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_log_success_win @@ -0,0 +1,320 @@ +UPDATE TYPE complete +PREPARE REMOVEFILE searchplugins/searchpluginstext0 +PREPARE REMOVEFILE searchplugins/searchpluginspng0.png +PREPARE REMOVEFILE removed-files +PREPARE REMOVEFILE precomplete +PREPARE REMOVEFILE exe0.exe +PREPARE REMOVEFILE 2/20/20text0 +PREPARE REMOVEFILE 2/20/20png0.png +PREPARE REMOVEFILE 0/0exe0.exe +PREPARE REMOVEFILE 0/00/00text0 +PREPARE REMOVEDIR searchplugins/ +PREPARE REMOVEDIR defaults/pref/ +PREPARE REMOVEDIR defaults/ +PREPARE REMOVEDIR 2/20/ +PREPARE REMOVEDIR 2/ +PREPARE REMOVEDIR 0/00/ +PREPARE REMOVEDIR 0/ +PREPARE ADD searchplugins/searchpluginstext0 +PREPARE ADD searchplugins/searchpluginspng1.png +PREPARE ADD searchplugins/searchpluginspng0.png +PREPARE ADD removed-files +PREPARE ADD precomplete +PREPARE ADD exe0.exe +PREPARE ADD distribution/extensions/extensions1/extensions1text0 +PREPARE ADD distribution/extensions/extensions1/extensions1png1.png +PREPARE ADD distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD distribution/extensions/extensions0/extensions0text0 +PREPARE ADD distribution/extensions/extensions0/extensions0png1.png +PREPARE ADD distribution/extensions/extensions0/extensions0png0.png +PREPARE ADD 1/10/10text0 +PREPARE ADD 0/0exe0.exe +PREPARE ADD 0/00/00text1 +PREPARE ADD 0/00/00text0 +PREPARE ADD 0/00/00png0.png +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/98/ +PREPARE REMOVEFILE 9/97/970/97xtext0 +PREPARE REMOVEFILE 9/97/970/97xtext1 +PREPARE REMOVEDIR 9/97/970/ +PREPARE REMOVEFILE 9/97/971/97xtext0 +PREPARE REMOVEFILE 9/97/971/97xtext1 +PREPARE REMOVEDIR 9/97/971/ +PREPARE REMOVEDIR 9/97/ +PREPARE REMOVEFILE 9/96/96text0 +PREPARE REMOVEFILE 9/96/96text1 +PREPARE REMOVEDIR 9/96/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/93/ +PREPARE REMOVEDIR 9/92/ +PREPARE REMOVEDIR 9/91/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/88/ +PREPARE REMOVEFILE 8/87/870/87xtext0 +PREPARE REMOVEFILE 8/87/870/87xtext1 +PREPARE REMOVEDIR 8/87/870/ +PREPARE REMOVEFILE 8/87/871/87xtext0 +PREPARE REMOVEFILE 8/87/871/87xtext1 +PREPARE REMOVEDIR 8/87/871/ +PREPARE REMOVEDIR 8/87/ +PREPARE REMOVEFILE 8/86/86text0 +PREPARE REMOVEFILE 8/86/86text1 +PREPARE REMOVEDIR 8/86/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/83/ +PREPARE REMOVEDIR 8/82/ +PREPARE REMOVEDIR 8/81/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEFILE 7/70/7xtest.exe +PREPARE REMOVEFILE 7/70/7xtext0 +PREPARE REMOVEFILE 7/70/7xtext1 +PREPARE REMOVEDIR 7/70/ +PREPARE REMOVEFILE 7/71/7xtest.exe +PREPARE REMOVEFILE 7/71/7xtext0 +PREPARE REMOVEFILE 7/71/7xtext1 +PREPARE REMOVEDIR 7/71/ +PREPARE REMOVEFILE 7/7text0 +PREPARE REMOVEFILE 7/7text1 +PREPARE REMOVEDIR 7/ +PREPARE REMOVEDIR 6/ +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5test.exe +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEDIR 5/ +PREPARE REMOVEFILE 4/4text1 +PREPARE REMOVEFILE 4/4text0 +PREPARE REMOVEDIR 4/ +PREPARE REMOVEFILE 3/3text1 +PREPARE REMOVEFILE 3/3text0 +EXECUTE REMOVEFILE searchplugins/searchpluginstext0 +EXECUTE REMOVEFILE searchplugins/searchpluginspng0.png +EXECUTE REMOVEFILE removed-files +EXECUTE REMOVEFILE precomplete +EXECUTE REMOVEFILE exe0.exe +EXECUTE REMOVEFILE 2/20/20text0 +EXECUTE REMOVEFILE 2/20/20png0.png +EXECUTE REMOVEFILE 0/0exe0.exe +EXECUTE REMOVEFILE 0/00/00text0 +EXECUTE REMOVEDIR searchplugins/ +EXECUTE REMOVEDIR defaults/pref/ +EXECUTE REMOVEDIR defaults/ +EXECUTE REMOVEDIR 2/20/ +EXECUTE REMOVEDIR 2/ +EXECUTE REMOVEDIR 0/00/ +EXECUTE REMOVEDIR 0/ +EXECUTE ADD searchplugins/searchpluginstext0 +EXECUTE ADD searchplugins/searchpluginspng1.png +EXECUTE ADD searchplugins/searchpluginspng0.png +EXECUTE ADD removed-files +EXECUTE ADD precomplete +EXECUTE ADD exe0.exe +EXECUTE ADD distribution/extensions/extensions1/extensions1text0 +EXECUTE ADD distribution/extensions/extensions1/extensions1png1.png +EXECUTE ADD distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD distribution/extensions/extensions0/extensions0text0 +EXECUTE ADD distribution/extensions/extensions0/extensions0png1.png +EXECUTE ADD distribution/extensions/extensions0/extensions0png0.png +EXECUTE ADD 1/10/10text0 +EXECUTE ADD 0/0exe0.exe +EXECUTE ADD 0/00/00text1 +EXECUTE ADD 0/00/00text0 +EXECUTE ADD 0/00/00png0.png +EXECUTE REMOVEDIR 9/99/ +EXECUTE REMOVEDIR 9/99/ +EXECUTE REMOVEDIR 9/98/ +EXECUTE REMOVEFILE 9/97/970/97xtext0 +EXECUTE REMOVEFILE 9/97/970/97xtext1 +EXECUTE REMOVEDIR 9/97/970/ +EXECUTE REMOVEFILE 9/97/971/97xtext0 +EXECUTE REMOVEFILE 9/97/971/97xtext1 +EXECUTE REMOVEDIR 9/97/971/ +EXECUTE REMOVEDIR 9/97/ +EXECUTE REMOVEFILE 9/96/96text0 +EXECUTE REMOVEFILE 9/96/96text1 +EXECUTE REMOVEDIR 9/96/ +EXECUTE REMOVEDIR 9/95/ +EXECUTE REMOVEDIR 9/95/ +EXECUTE REMOVEDIR 9/94/ +EXECUTE REMOVEDIR 9/94/ +EXECUTE REMOVEDIR 9/93/ +EXECUTE REMOVEDIR 9/92/ +EXECUTE REMOVEDIR 9/91/ +EXECUTE REMOVEDIR 9/90/ +EXECUTE REMOVEDIR 9/90/ +EXECUTE REMOVEDIR 8/89/ +EXECUTE REMOVEDIR 8/89/ +EXECUTE REMOVEDIR 8/88/ +EXECUTE REMOVEFILE 8/87/870/87xtext0 +EXECUTE REMOVEFILE 8/87/870/87xtext1 +EXECUTE REMOVEDIR 8/87/870/ +EXECUTE REMOVEFILE 8/87/871/87xtext0 +EXECUTE REMOVEFILE 8/87/871/87xtext1 +EXECUTE REMOVEDIR 8/87/871/ +EXECUTE REMOVEDIR 8/87/ +EXECUTE REMOVEFILE 8/86/86text0 +EXECUTE REMOVEFILE 8/86/86text1 +EXECUTE REMOVEDIR 8/86/ +EXECUTE REMOVEDIR 8/85/ +EXECUTE REMOVEDIR 8/85/ +EXECUTE REMOVEDIR 8/84/ +EXECUTE REMOVEDIR 8/84/ +EXECUTE REMOVEDIR 8/83/ +EXECUTE REMOVEDIR 8/82/ +EXECUTE REMOVEDIR 8/81/ +EXECUTE REMOVEDIR 8/80/ +EXECUTE REMOVEDIR 8/80/ +EXECUTE REMOVEFILE 7/70/7xtest.exe +EXECUTE REMOVEFILE 7/70/7xtext0 +EXECUTE REMOVEFILE 7/70/7xtext1 +EXECUTE REMOVEDIR 7/70/ +EXECUTE REMOVEFILE 7/71/7xtest.exe +EXECUTE REMOVEFILE 7/71/7xtext0 +EXECUTE REMOVEFILE 7/71/7xtext1 +EXECUTE REMOVEDIR 7/71/ +EXECUTE REMOVEFILE 7/7text0 +EXECUTE REMOVEFILE 7/7text1 +EXECUTE REMOVEDIR 7/ +EXECUTE REMOVEDIR 6/ +EXECUTE REMOVEFILE 5/5text1 +EXECUTE REMOVEFILE 5/5text0 +EXECUTE REMOVEFILE 5/5test.exe +EXECUTE REMOVEFILE 5/5text0 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEFILE 5/5text1 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEDIR 5/ +EXECUTE REMOVEFILE 4/4text1 +EXECUTE REMOVEFILE 4/4text0 +EXECUTE REMOVEDIR 4/ +EXECUTE REMOVEFILE 3/3text1 +EXECUTE REMOVEFILE 3/3text0 +FINISH REMOVEFILE searchplugins/searchpluginstext0 +FINISH REMOVEFILE searchplugins/searchpluginspng0.png +FINISH REMOVEFILE removed-files +FINISH REMOVEFILE precomplete +FINISH REMOVEFILE exe0.exe +FINISH REMOVEFILE 2/20/20text0 +FINISH REMOVEFILE 2/20/20png0.png +FINISH REMOVEFILE 0/0exe0.exe +FINISH REMOVEFILE 0/00/00text0 +FINISH REMOVEDIR searchplugins/ +removing directory: searchplugins/, rv: 0 +FINISH REMOVEDIR defaults/pref/ +removing directory: defaults/pref/, rv: 0 +FINISH REMOVEDIR defaults/ +removing directory: defaults/, rv: 0 +FINISH REMOVEDIR 2/20/ +FINISH REMOVEDIR 2/ +FINISH REMOVEDIR 0/00/ +removing directory: 0/00/, rv: 0 +FINISH REMOVEDIR 0/ +removing directory: 0/, rv: 0 +FINISH ADD searchplugins/searchpluginstext0 +FINISH ADD searchplugins/searchpluginspng1.png +FINISH ADD searchplugins/searchpluginspng0.png +FINISH ADD removed-files +FINISH ADD precomplete +FINISH ADD exe0.exe +FINISH ADD distribution/extensions/extensions1/extensions1text0 +FINISH ADD distribution/extensions/extensions1/extensions1png1.png +FINISH ADD distribution/extensions/extensions1/extensions1png0.png +FINISH ADD distribution/extensions/extensions0/extensions0text0 +FINISH ADD distribution/extensions/extensions0/extensions0png1.png +FINISH ADD distribution/extensions/extensions0/extensions0png0.png +FINISH ADD 1/10/10text0 +FINISH ADD 0/0exe0.exe +FINISH ADD 0/00/00text1 +FINISH ADD 0/00/00text0 +FINISH ADD 0/00/00png0.png +FINISH REMOVEDIR 9/99/ +FINISH REMOVEDIR 9/99/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/98/ +FINISH REMOVEFILE 9/97/970/97xtext0 +FINISH REMOVEFILE 9/97/970/97xtext1 +FINISH REMOVEDIR 9/97/970/ +FINISH REMOVEFILE 9/97/971/97xtext0 +FINISH REMOVEFILE 9/97/971/97xtext1 +FINISH REMOVEDIR 9/97/971/ +FINISH REMOVEDIR 9/97/ +FINISH REMOVEFILE 9/96/96text0 +FINISH REMOVEFILE 9/96/96text1 +FINISH REMOVEDIR 9/96/ +FINISH REMOVEDIR 9/95/ +FINISH REMOVEDIR 9/95/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/94/ +FINISH REMOVEDIR 9/94/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/93/ +FINISH REMOVEDIR 9/92/ +removing directory: 9/92/, rv: 0 +FINISH REMOVEDIR 9/91/ +removing directory: 9/91/, rv: 0 +FINISH REMOVEDIR 9/90/ +FINISH REMOVEDIR 9/90/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/89/ +FINISH REMOVEDIR 8/89/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/88/ +FINISH REMOVEFILE 8/87/870/87xtext0 +FINISH REMOVEFILE 8/87/870/87xtext1 +FINISH REMOVEDIR 8/87/870/ +FINISH REMOVEFILE 8/87/871/87xtext0 +FINISH REMOVEFILE 8/87/871/87xtext1 +FINISH REMOVEDIR 8/87/871/ +FINISH REMOVEDIR 8/87/ +FINISH REMOVEFILE 8/86/86text0 +FINISH REMOVEFILE 8/86/86text1 +FINISH REMOVEDIR 8/86/ +FINISH REMOVEDIR 8/85/ +FINISH REMOVEDIR 8/85/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/84/ +FINISH REMOVEDIR 8/84/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/83/ +FINISH REMOVEDIR 8/82/ +removing directory: 8/82/, rv: 0 +FINISH REMOVEDIR 8/81/ +removing directory: 8/81/, rv: 0 +FINISH REMOVEDIR 8/80/ +FINISH REMOVEDIR 8/80/ +directory no longer exists; skipping +FINISH REMOVEFILE 7/70/7xtest.exe +FINISH REMOVEFILE 7/70/7xtext0 +FINISH REMOVEFILE 7/70/7xtext1 +FINISH REMOVEDIR 7/70/ +FINISH REMOVEFILE 7/71/7xtest.exe +FINISH REMOVEFILE 7/71/7xtext0 +FINISH REMOVEFILE 7/71/7xtext1 +FINISH REMOVEDIR 7/71/ +FINISH REMOVEFILE 7/7text0 +FINISH REMOVEFILE 7/7text1 +FINISH REMOVEDIR 7/ +FINISH REMOVEDIR 6/ +FINISH REMOVEFILE 5/5text1 +FINISH REMOVEFILE 5/5text0 +FINISH REMOVEFILE 5/5test.exe +FINISH REMOVEDIR 5/ +FINISH REMOVEFILE 4/4text1 +FINISH REMOVEFILE 4/4text0 +FINISH REMOVEDIR 4/ +FINISH REMOVEFILE 3/3text1 +FINISH REMOVEFILE 3/3text0 +succeeded +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/complete_mac.mar b/toolkit/mozapps/update/tests/data/complete_mac.mar Binary files differnew file mode 100644 index 000000000..ca1497f4f --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_mac.mar diff --git a/toolkit/mozapps/update/tests/data/complete_precomplete b/toolkit/mozapps/update/tests/data/complete_precomplete new file mode 100644 index 000000000..ae7a0013f --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_precomplete @@ -0,0 +1,18 @@ +remove "searchplugins/searchpluginstext0" +remove "searchplugins/searchpluginspng1.png" +remove "searchplugins/searchpluginspng0.png" +remove "removed-files" +remove "precomplete" +remove "exe0.exe" +remove "1/10/10text0" +remove "0/0exe0.exe" +remove "0/00/00text1" +remove "0/00/00text0" +remove "0/00/00png0.png" +rmdir "searchplugins/" +rmdir "defaults/pref/" +rmdir "defaults/" +rmdir "1/10/" +rmdir "1/" +rmdir "0/00/" +rmdir "0/" diff --git a/toolkit/mozapps/update/tests/data/complete_precomplete_mac b/toolkit/mozapps/update/tests/data/complete_precomplete_mac new file mode 100644 index 000000000..8d81a36d6 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_precomplete_mac @@ -0,0 +1,21 @@ +remove "Contents/Resources/searchplugins/searchpluginstext0" +remove "Contents/Resources/searchplugins/searchpluginspng1.png" +remove "Contents/Resources/searchplugins/searchpluginspng0.png" +remove "Contents/Resources/removed-files" +remove "Contents/Resources/precomplete" +remove "Contents/Resources/1/10/10text0" +remove "Contents/Resources/0/0exe0.exe" +remove "Contents/Resources/0/00/00text1" +remove "Contents/Resources/0/00/00text0" +remove "Contents/Resources/0/00/00png0.png" +remove "Contents/MacOS/exe0.exe" +rmdir "Contents/Resources/searchplugins/" +rmdir "Contents/Resources/defaults/pref/" +rmdir "Contents/Resources/defaults/" +rmdir "Contents/Resources/1/10/" +rmdir "Contents/Resources/1/" +rmdir "Contents/Resources/0/00/" +rmdir "Contents/Resources/0/" +rmdir "Contents/Resources/" +rmdir "Contents/MacOS/" +rmdir "Contents/" diff --git a/toolkit/mozapps/update/tests/data/complete_removed-files b/toolkit/mozapps/update/tests/data/complete_removed-files new file mode 100644 index 000000000..e45c43c1f --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_removed-files @@ -0,0 +1,41 @@ +text0 +text1 +3/3text0 +3/3text1 +4/exe0.exe +4/4text0 +4/4text1 +4/ +5/5text0 +5/5text1 +5/* +6/ +7/* +8/80/ +8/81/ +8/82/ +8/83/ +8/84/ +8/85/* +8/86/* +8/87/* +8/88/* +8/89/* +8/80/ +8/84/* +8/85/* +8/89/ +9/90/ +9/91/ +9/92/ +9/93/ +9/94/ +9/95/* +9/96/* +9/97/* +9/98/* +9/99/* +9/90/ +9/94/* +9/95/* +9/99/ diff --git a/toolkit/mozapps/update/tests/data/complete_removed-files_mac b/toolkit/mozapps/update/tests/data/complete_removed-files_mac new file mode 100644 index 000000000..955dc5b34 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_removed-files_mac @@ -0,0 +1,41 @@ +Contents/Resources/text0 +Contents/Resources/text1 +Contents/Resources/3/3text0 +Contents/Resources/3/3text1 +Contents/Resources/4/exe0.exe +Contents/Resources/4/4text0 +Contents/Resources/4/4text1 +Contents/Resources/4/ +Contents/Resources/5/5text0 +Contents/Resources/5/5text1 +Contents/Resources/5/* +Contents/Resources/6/ +Contents/Resources/7/* +Contents/Resources/8/80/ +Contents/Resources/8/81/ +Contents/Resources/8/82/ +Contents/Resources/8/83/ +Contents/Resources/8/84/ +Contents/Resources/8/85/* +Contents/Resources/8/86/* +Contents/Resources/8/87/* +Contents/Resources/8/88/* +Contents/Resources/8/89/* +Contents/Resources/8/80/ +Contents/Resources/8/84/* +Contents/Resources/8/85/* +Contents/Resources/8/89/ +Contents/Resources/9/90/ +Contents/Resources/9/91/ +Contents/Resources/9/92/ +Contents/Resources/9/93/ +Contents/Resources/9/94/ +Contents/Resources/9/95/* +Contents/Resources/9/96/* +Contents/Resources/9/97/* +Contents/Resources/9/98/* +Contents/Resources/9/99/* +Contents/Resources/9/90/ +Contents/Resources/9/94/* +Contents/Resources/9/95/* +Contents/Resources/9/99/ diff --git a/toolkit/mozapps/update/tests/data/complete_update_manifest b/toolkit/mozapps/update/tests/data/complete_update_manifest new file mode 100644 index 000000000..383a324f6 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_update_manifest @@ -0,0 +1,59 @@ +type "complete" +add "precomplete" +add "searchplugins/searchpluginstext0" +add "searchplugins/searchpluginspng1.png" +add "searchplugins/searchpluginspng0.png" +add "removed-files" +add-if "extensions/extensions1" "extensions/extensions1/extensions1text0" +add-if "extensions/extensions1" "extensions/extensions1/extensions1png1.png" +add-if "extensions/extensions1" "extensions/extensions1/extensions1png0.png" +add-if "extensions/extensions0" "extensions/extensions0/extensions0text0" +add-if "extensions/extensions0" "extensions/extensions0/extensions0png1.png" +add-if "extensions/extensions0" "extensions/extensions0/extensions0png0.png" +add "exe0.exe" +add "1/10/10text0" +add "0/0exe0.exe" +add "0/00/00text1" +add "0/00/00text0" +add "0/00/00png0.png" +remove "text1" +remove "text0" +rmrfdir "9/99/" +rmdir "9/99/" +rmrfdir "9/98/" +rmrfdir "9/97/" +rmrfdir "9/96/" +rmrfdir "9/95/" +rmrfdir "9/95/" +rmrfdir "9/94/" +rmdir "9/94/" +rmdir "9/93/" +rmdir "9/92/" +rmdir "9/91/" +rmdir "9/90/" +rmdir "9/90/" +rmrfdir "8/89/" +rmdir "8/89/" +rmrfdir "8/88/" +rmrfdir "8/87/" +rmrfdir "8/86/" +rmrfdir "8/85/" +rmrfdir "8/85/" +rmrfdir "8/84/" +rmdir "8/84/" +rmdir "8/83/" +rmdir "8/82/" +rmdir "8/81/" +rmdir "8/80/" +rmdir "8/80/" +rmrfdir "7/" +rmdir "6/" +remove "5/5text1" +remove "5/5text0" +rmrfdir "5/" +remove "4/exe0.exe" +remove "4/4text1" +remove "4/4text0" +rmdir "4/" +remove "3/3text1" +remove "3/3text0" diff --git a/toolkit/mozapps/update/tests/data/old_version.mar b/toolkit/mozapps/update/tests/data/old_version.mar Binary files differnew file mode 100644 index 000000000..31550698a --- /dev/null +++ b/toolkit/mozapps/update/tests/data/old_version.mar diff --git a/toolkit/mozapps/update/tests/data/partial.exe b/toolkit/mozapps/update/tests/data/partial.exe Binary files differnew file mode 100644 index 000000000..3949fd2a0 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial.exe diff --git a/toolkit/mozapps/update/tests/data/partial.mar b/toolkit/mozapps/update/tests/data/partial.mar Binary files differnew file mode 100644 index 000000000..789d3d98d --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial.mar diff --git a/toolkit/mozapps/update/tests/data/partial.png b/toolkit/mozapps/update/tests/data/partial.png Binary files differnew file mode 100644 index 000000000..9246f586c --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial.png diff --git a/toolkit/mozapps/update/tests/data/partial_log_failure_mac b/toolkit/mozapps/update/tests/data/partial_log_failure_mac new file mode 100644 index 000000000..3b2933ebd --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_log_failure_mac @@ -0,0 +1,192 @@ +UPDATE TYPE partial +PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0 +PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng1.png +PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng0.png +PREPARE ADD Contents/Resources/precomplete +PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +PREPARE PATCH Contents/Resources/0/0exe0.exe +PREPARE ADD Contents/Resources/0/00/00text0 +PREPARE PATCH Contents/Resources/0/00/00png0.png +PREPARE PATCH Contents/MacOS/exe0.exe +PREPARE ADD Contents/Resources/2/20/20text0 +PREPARE ADD Contents/Resources/2/20/20png0.png +PREPARE ADD Contents/Resources/0/00/00text2 +PREPARE REMOVEFILE Contents/Resources/1/10/10text0 +PREPARE REMOVEFILE Contents/Resources/0/00/00text1 +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/98/ +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/970/ +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/971/ +PREPARE REMOVEDIR Contents/Resources/9/97/ +PREPARE REMOVEFILE Contents/Resources/9/96/96text0 +PREPARE REMOVEFILE Contents/Resources/9/96/96text1 +PREPARE REMOVEDIR Contents/Resources/9/96/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/93/ +PREPARE REMOVEDIR Contents/Resources/9/92/ +PREPARE REMOVEDIR Contents/Resources/9/91/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/88/ +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/870/ +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/871/ +PREPARE REMOVEDIR Contents/Resources/8/87/ +PREPARE REMOVEFILE Contents/Resources/8/86/86text0 +PREPARE REMOVEFILE Contents/Resources/8/86/86text1 +PREPARE REMOVEDIR Contents/Resources/8/86/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/83/ +PREPARE REMOVEDIR Contents/Resources/8/82/ +PREPARE REMOVEDIR Contents/Resources/8/81/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/70/ +PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/71/ +PREPARE REMOVEFILE Contents/Resources/7/7text0 +PREPARE REMOVEFILE Contents/Resources/7/7text1 +PREPARE REMOVEDIR Contents/Resources/7/ +PREPARE REMOVEDIR Contents/Resources/6/ +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5test.exe +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEDIR Contents/Resources/5/ +PREPARE REMOVEFILE Contents/Resources/4/4text1 +PREPARE REMOVEFILE Contents/Resources/4/4text0 +PREPARE REMOVEDIR Contents/Resources/4/ +PREPARE REMOVEFILE Contents/Resources/3/3text1 +PREPARE REMOVEFILE Contents/Resources/3/3text0 +PREPARE REMOVEDIR Contents/Resources/1/10/ +PREPARE REMOVEDIR Contents/Resources/1/ +EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0 +EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng1.png +EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng0.png +EXECUTE ADD Contents/Resources/precomplete +EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +EXECUTE PATCH Contents/Resources/0/0exe0.exe +LoadSourceFile: destination file size 776 does not match expected size 79872 +LoadSourceFile failed +### execution failed +FINISH ADD Contents/Resources/searchplugins/searchpluginstext0 +FINISH PATCH Contents/Resources/searchplugins/searchpluginspng1.png +FINISH PATCH Contents/Resources/searchplugins/searchpluginspng0.png +FINISH ADD Contents/Resources/precomplete +FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +backup_restore: backup file doesn't exist: Contents/Resources/distribution/extensions/extensions1/extensions1text0.moz-backup +FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +FINISH PATCH Contents/Resources/0/0exe0.exe +backup_restore: backup file doesn't exist: Contents/Resources/0/0exe0.exe.moz-backup +FINISH ADD Contents/Resources/0/00/00text0 +backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text0.moz-backup +FINISH PATCH Contents/Resources/0/00/00png0.png +backup_restore: backup file doesn't exist: Contents/Resources/0/00/00png0.png.moz-backup +FINISH PATCH Contents/MacOS/exe0.exe +backup_restore: backup file doesn't exist: Contents/MacOS/exe0.exe.moz-backup +FINISH ADD Contents/Resources/2/20/20text0 +backup_restore: backup file doesn't exist: Contents/Resources/2/20/20text0.moz-backup +FINISH ADD Contents/Resources/2/20/20png0.png +backup_restore: backup file doesn't exist: Contents/Resources/2/20/20png0.png.moz-backup +FINISH ADD Contents/Resources/0/00/00text2 +backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text2.moz-backup +FINISH REMOVEFILE Contents/Resources/1/10/10text0 +backup_restore: backup file doesn't exist: Contents/Resources/1/10/10text0.moz-backup +FINISH REMOVEFILE Contents/Resources/0/00/00text1 +backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text1.moz-backup +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/9/97/970/97xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/9/97/970/97xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/9/97/971/97xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/9/97/971/97xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/9/96/96text0 +backup_restore: backup file doesn't exist: Contents/Resources/9/96/96text0.moz-backup +FINISH REMOVEFILE Contents/Resources/9/96/96text1 +backup_restore: backup file doesn't exist: Contents/Resources/9/96/96text1.moz-backup +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/8/87/870/87xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/8/87/870/87xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/8/87/871/87xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/8/87/871/87xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/8/86/86text0 +backup_restore: backup file doesn't exist: Contents/Resources/8/86/86text0.moz-backup +FINISH REMOVEFILE Contents/Resources/8/86/86text1 +backup_restore: backup file doesn't exist: Contents/Resources/8/86/86text1.moz-backup +FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe +backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtest.exe.moz-backup +FINISH REMOVEFILE Contents/Resources/7/70/7xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/7/70/7xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe +backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtest.exe.moz-backup +FINISH REMOVEFILE Contents/Resources/7/71/7xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/7/71/7xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/7/7text0 +backup_restore: backup file doesn't exist: Contents/Resources/7/7text0.moz-backup +FINISH REMOVEFILE Contents/Resources/7/7text1 +backup_restore: backup file doesn't exist: Contents/Resources/7/7text1.moz-backup +FINISH REMOVEFILE Contents/Resources/5/5text1 +backup_restore: backup file doesn't exist: Contents/Resources/5/5text1.moz-backup +FINISH REMOVEFILE Contents/Resources/5/5text0 +backup_restore: backup file doesn't exist: Contents/Resources/5/5text0.moz-backup +FINISH REMOVEFILE Contents/Resources/5/5test.exe +backup_restore: backup file doesn't exist: Contents/Resources/5/5test.exe.moz-backup +FINISH REMOVEFILE Contents/Resources/5/5text0 +backup_restore: backup file doesn't exist: Contents/Resources/5/5text0.moz-backup +FINISH REMOVEFILE Contents/Resources/5/5text1 +backup_restore: backup file doesn't exist: Contents/Resources/5/5text1.moz-backup +FINISH REMOVEFILE Contents/Resources/4/4text1 +backup_restore: backup file doesn't exist: Contents/Resources/4/4text1.moz-backup +FINISH REMOVEFILE Contents/Resources/4/4text0 +backup_restore: backup file doesn't exist: Contents/Resources/4/4text0.moz-backup +FINISH REMOVEFILE Contents/Resources/3/3text1 +backup_restore: backup file doesn't exist: Contents/Resources/3/3text1.moz-backup +FINISH REMOVEFILE Contents/Resources/3/3text0 +backup_restore: backup file doesn't exist: Contents/Resources/3/3text0.moz-backup +failed: 2 +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/partial_log_failure_win b/toolkit/mozapps/update/tests/data/partial_log_failure_win new file mode 100644 index 000000000..e3d683dc1 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_log_failure_win @@ -0,0 +1,192 @@ +UPDATE TYPE partial +PREPARE ADD searchplugins/searchpluginstext0 +PREPARE PATCH searchplugins/searchpluginspng1.png +PREPARE PATCH searchplugins/searchpluginspng0.png +PREPARE ADD precomplete +PREPARE PATCH exe0.exe +PREPARE ADD distribution/extensions/extensions1/extensions1text0 +PREPARE PATCH distribution/extensions/extensions1/extensions1png1.png +PREPARE PATCH distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD distribution/extensions/extensions0/extensions0text0 +PREPARE PATCH distribution/extensions/extensions0/extensions0png1.png +PREPARE PATCH distribution/extensions/extensions0/extensions0png0.png +PREPARE PATCH 0/0exe0.exe +PREPARE ADD 0/00/00text0 +PREPARE PATCH 0/00/00png0.png +PREPARE ADD 2/20/20text0 +PREPARE ADD 2/20/20png0.png +PREPARE ADD 0/00/00text2 +PREPARE REMOVEFILE 1/10/10text0 +PREPARE REMOVEFILE 0/00/00text1 +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/98/ +PREPARE REMOVEFILE 9/97/970/97xtext0 +PREPARE REMOVEFILE 9/97/970/97xtext1 +PREPARE REMOVEDIR 9/97/970/ +PREPARE REMOVEFILE 9/97/971/97xtext0 +PREPARE REMOVEFILE 9/97/971/97xtext1 +PREPARE REMOVEDIR 9/97/971/ +PREPARE REMOVEDIR 9/97/ +PREPARE REMOVEFILE 9/96/96text0 +PREPARE REMOVEFILE 9/96/96text1 +PREPARE REMOVEDIR 9/96/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/93/ +PREPARE REMOVEDIR 9/92/ +PREPARE REMOVEDIR 9/91/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/88/ +PREPARE REMOVEFILE 8/87/870/87xtext0 +PREPARE REMOVEFILE 8/87/870/87xtext1 +PREPARE REMOVEDIR 8/87/870/ +PREPARE REMOVEFILE 8/87/871/87xtext0 +PREPARE REMOVEFILE 8/87/871/87xtext1 +PREPARE REMOVEDIR 8/87/871/ +PREPARE REMOVEDIR 8/87/ +PREPARE REMOVEFILE 8/86/86text0 +PREPARE REMOVEFILE 8/86/86text1 +PREPARE REMOVEDIR 8/86/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/83/ +PREPARE REMOVEDIR 8/82/ +PREPARE REMOVEDIR 8/81/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEFILE 7/70/7xtest.exe +PREPARE REMOVEFILE 7/70/7xtext0 +PREPARE REMOVEFILE 7/70/7xtext1 +PREPARE REMOVEDIR 7/70/ +PREPARE REMOVEFILE 7/71/7xtest.exe +PREPARE REMOVEFILE 7/71/7xtext0 +PREPARE REMOVEFILE 7/71/7xtext1 +PREPARE REMOVEDIR 7/71/ +PREPARE REMOVEFILE 7/7text0 +PREPARE REMOVEFILE 7/7text1 +PREPARE REMOVEDIR 7/ +PREPARE REMOVEDIR 6/ +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5test.exe +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEDIR 5/ +PREPARE REMOVEFILE 4/4text1 +PREPARE REMOVEFILE 4/4text0 +PREPARE REMOVEDIR 4/ +PREPARE REMOVEFILE 3/3text1 +PREPARE REMOVEFILE 3/3text0 +PREPARE REMOVEDIR 1/10/ +PREPARE REMOVEDIR 1/ +EXECUTE ADD searchplugins/searchpluginstext0 +EXECUTE PATCH searchplugins/searchpluginspng1.png +EXECUTE PATCH searchplugins/searchpluginspng0.png +EXECUTE ADD precomplete +EXECUTE PATCH exe0.exe +EXECUTE ADD distribution/extensions/extensions1/extensions1text0 +EXECUTE PATCH distribution/extensions/extensions1/extensions1png1.png +EXECUTE PATCH distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD distribution/extensions/extensions0/extensions0text0 +EXECUTE PATCH distribution/extensions/extensions0/extensions0png1.png +EXECUTE PATCH distribution/extensions/extensions0/extensions0png0.png +EXECUTE PATCH 0/0exe0.exe +LoadSourceFile: destination file size 776 does not match expected size 79872 +LoadSourceFile failed +### execution failed +FINISH ADD searchplugins/searchpluginstext0 +FINISH PATCH searchplugins/searchpluginspng1.png +FINISH PATCH searchplugins/searchpluginspng0.png +FINISH ADD precomplete +FINISH PATCH exe0.exe +FINISH ADD distribution/extensions/extensions1/extensions1text0 +backup_restore: backup file doesn't exist: distribution/extensions/extensions1/extensions1text0.moz-backup +FINISH PATCH distribution/extensions/extensions1/extensions1png1.png +FINISH PATCH distribution/extensions/extensions1/extensions1png0.png +FINISH ADD distribution/extensions/extensions0/extensions0text0 +FINISH PATCH distribution/extensions/extensions0/extensions0png1.png +FINISH PATCH distribution/extensions/extensions0/extensions0png0.png +FINISH PATCH 0/0exe0.exe +backup_restore: backup file doesn't exist: 0/0exe0.exe.moz-backup +FINISH ADD 0/00/00text0 +backup_restore: backup file doesn't exist: 0/00/00text0.moz-backup +FINISH PATCH 0/00/00png0.png +backup_restore: backup file doesn't exist: 0/00/00png0.png.moz-backup +FINISH ADD 2/20/20text0 +backup_restore: backup file doesn't exist: 2/20/20text0.moz-backup +FINISH ADD 2/20/20png0.png +backup_restore: backup file doesn't exist: 2/20/20png0.png.moz-backup +FINISH ADD 0/00/00text2 +backup_restore: backup file doesn't exist: 0/00/00text2.moz-backup +FINISH REMOVEFILE 1/10/10text0 +backup_restore: backup file doesn't exist: 1/10/10text0.moz-backup +FINISH REMOVEFILE 0/00/00text1 +backup_restore: backup file doesn't exist: 0/00/00text1.moz-backup +FINISH REMOVEFILE 9/97/970/97xtext0 +backup_restore: backup file doesn't exist: 9/97/970/97xtext0.moz-backup +FINISH REMOVEFILE 9/97/970/97xtext1 +backup_restore: backup file doesn't exist: 9/97/970/97xtext1.moz-backup +FINISH REMOVEFILE 9/97/971/97xtext0 +backup_restore: backup file doesn't exist: 9/97/971/97xtext0.moz-backup +FINISH REMOVEFILE 9/97/971/97xtext1 +backup_restore: backup file doesn't exist: 9/97/971/97xtext1.moz-backup +FINISH REMOVEFILE 9/96/96text0 +backup_restore: backup file doesn't exist: 9/96/96text0.moz-backup +FINISH REMOVEFILE 9/96/96text1 +backup_restore: backup file doesn't exist: 9/96/96text1.moz-backup +FINISH REMOVEFILE 8/87/870/87xtext0 +backup_restore: backup file doesn't exist: 8/87/870/87xtext0.moz-backup +FINISH REMOVEFILE 8/87/870/87xtext1 +backup_restore: backup file doesn't exist: 8/87/870/87xtext1.moz-backup +FINISH REMOVEFILE 8/87/871/87xtext0 +backup_restore: backup file doesn't exist: 8/87/871/87xtext0.moz-backup +FINISH REMOVEFILE 8/87/871/87xtext1 +backup_restore: backup file doesn't exist: 8/87/871/87xtext1.moz-backup +FINISH REMOVEFILE 8/86/86text0 +backup_restore: backup file doesn't exist: 8/86/86text0.moz-backup +FINISH REMOVEFILE 8/86/86text1 +backup_restore: backup file doesn't exist: 8/86/86text1.moz-backup +FINISH REMOVEFILE 7/70/7xtest.exe +backup_restore: backup file doesn't exist: 7/70/7xtest.exe.moz-backup +FINISH REMOVEFILE 7/70/7xtext0 +backup_restore: backup file doesn't exist: 7/70/7xtext0.moz-backup +FINISH REMOVEFILE 7/70/7xtext1 +backup_restore: backup file doesn't exist: 7/70/7xtext1.moz-backup +FINISH REMOVEFILE 7/71/7xtest.exe +backup_restore: backup file doesn't exist: 7/71/7xtest.exe.moz-backup +FINISH REMOVEFILE 7/71/7xtext0 +backup_restore: backup file doesn't exist: 7/71/7xtext0.moz-backup +FINISH REMOVEFILE 7/71/7xtext1 +backup_restore: backup file doesn't exist: 7/71/7xtext1.moz-backup +FINISH REMOVEFILE 7/7text0 +backup_restore: backup file doesn't exist: 7/7text0.moz-backup +FINISH REMOVEFILE 7/7text1 +backup_restore: backup file doesn't exist: 7/7text1.moz-backup +FINISH REMOVEFILE 5/5text1 +backup_restore: backup file doesn't exist: 5/5text1.moz-backup +FINISH REMOVEFILE 5/5text0 +backup_restore: backup file doesn't exist: 5/5text0.moz-backup +FINISH REMOVEFILE 5/5test.exe +backup_restore: backup file doesn't exist: 5/5test.exe.moz-backup +FINISH REMOVEFILE 5/5text0 +backup_restore: backup file doesn't exist: 5/5text0.moz-backup +FINISH REMOVEFILE 5/5text1 +backup_restore: backup file doesn't exist: 5/5text1.moz-backup +FINISH REMOVEFILE 4/4text1 +backup_restore: backup file doesn't exist: 4/4text1.moz-backup +FINISH REMOVEFILE 4/4text0 +backup_restore: backup file doesn't exist: 4/4text0.moz-backup +FINISH REMOVEFILE 3/3text1 +backup_restore: backup file doesn't exist: 3/3text1.moz-backup +FINISH REMOVEFILE 3/3text0 +backup_restore: backup file doesn't exist: 3/3text0.moz-backup +failed: 2 +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/partial_log_success_mac b/toolkit/mozapps/update/tests/data/partial_log_success_mac new file mode 100644 index 000000000..fb5272ad2 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_log_success_mac @@ -0,0 +1,279 @@ +UPDATE TYPE partial +PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0 +PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng1.png +PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng0.png +PREPARE ADD Contents/Resources/precomplete +PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +PREPARE PATCH Contents/Resources/0/0exe0.exe +PREPARE ADD Contents/Resources/0/00/00text0 +PREPARE PATCH Contents/Resources/0/00/00png0.png +PREPARE PATCH Contents/MacOS/exe0.exe +PREPARE ADD Contents/Resources/2/20/20text0 +PREPARE ADD Contents/Resources/2/20/20png0.png +PREPARE ADD Contents/Resources/0/00/00text2 +PREPARE REMOVEFILE Contents/Resources/1/10/10text0 +PREPARE REMOVEFILE Contents/Resources/0/00/00text1 +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/98/ +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/970/ +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/971/ +PREPARE REMOVEDIR Contents/Resources/9/97/ +PREPARE REMOVEFILE Contents/Resources/9/96/96text0 +PREPARE REMOVEFILE Contents/Resources/9/96/96text1 +PREPARE REMOVEDIR Contents/Resources/9/96/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/93/ +PREPARE REMOVEDIR Contents/Resources/9/92/ +PREPARE REMOVEDIR Contents/Resources/9/91/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/88/ +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/870/ +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/871/ +PREPARE REMOVEDIR Contents/Resources/8/87/ +PREPARE REMOVEFILE Contents/Resources/8/86/86text0 +PREPARE REMOVEFILE Contents/Resources/8/86/86text1 +PREPARE REMOVEDIR Contents/Resources/8/86/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/83/ +PREPARE REMOVEDIR Contents/Resources/8/82/ +PREPARE REMOVEDIR Contents/Resources/8/81/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/70/ +PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/71/ +PREPARE REMOVEFILE Contents/Resources/7/7text0 +PREPARE REMOVEFILE Contents/Resources/7/7text1 +PREPARE REMOVEDIR Contents/Resources/7/ +PREPARE REMOVEDIR Contents/Resources/6/ +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5test.exe +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEDIR Contents/Resources/5/ +PREPARE REMOVEFILE Contents/Resources/4/4text1 +PREPARE REMOVEFILE Contents/Resources/4/4text0 +PREPARE REMOVEDIR Contents/Resources/4/ +PREPARE REMOVEFILE Contents/Resources/3/3text1 +PREPARE REMOVEFILE Contents/Resources/3/3text0 +PREPARE REMOVEDIR Contents/Resources/1/10/ +PREPARE REMOVEDIR Contents/Resources/1/ +EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0 +EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng1.png +EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng0.png +EXECUTE ADD Contents/Resources/precomplete +EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +EXECUTE PATCH Contents/Resources/0/0exe0.exe +EXECUTE ADD Contents/Resources/0/00/00text0 +EXECUTE PATCH Contents/Resources/0/00/00png0.png +EXECUTE PATCH Contents/MacOS/exe0.exe +EXECUTE ADD Contents/Resources/2/20/20text0 +EXECUTE ADD Contents/Resources/2/20/20png0.png +EXECUTE ADD Contents/Resources/0/00/00text2 +EXECUTE REMOVEFILE Contents/Resources/1/10/10text0 +EXECUTE REMOVEFILE Contents/Resources/0/00/00text1 +EXECUTE REMOVEDIR Contents/Resources/9/99/ +EXECUTE REMOVEDIR Contents/Resources/9/99/ +EXECUTE REMOVEDIR Contents/Resources/9/98/ +EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext0 +EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext1 +EXECUTE REMOVEDIR Contents/Resources/9/97/970/ +EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext0 +EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext1 +EXECUTE REMOVEDIR Contents/Resources/9/97/971/ +EXECUTE REMOVEDIR Contents/Resources/9/97/ +EXECUTE REMOVEFILE Contents/Resources/9/96/96text0 +EXECUTE REMOVEFILE Contents/Resources/9/96/96text1 +EXECUTE REMOVEDIR Contents/Resources/9/96/ +EXECUTE REMOVEDIR Contents/Resources/9/95/ +EXECUTE REMOVEDIR Contents/Resources/9/95/ +EXECUTE REMOVEDIR Contents/Resources/9/94/ +EXECUTE REMOVEDIR Contents/Resources/9/94/ +EXECUTE REMOVEDIR Contents/Resources/9/93/ +EXECUTE REMOVEDIR Contents/Resources/9/92/ +EXECUTE REMOVEDIR Contents/Resources/9/91/ +EXECUTE REMOVEDIR Contents/Resources/9/90/ +EXECUTE REMOVEDIR Contents/Resources/9/90/ +EXECUTE REMOVEDIR Contents/Resources/8/89/ +EXECUTE REMOVEDIR Contents/Resources/8/89/ +EXECUTE REMOVEDIR Contents/Resources/8/88/ +EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext0 +EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext1 +EXECUTE REMOVEDIR Contents/Resources/8/87/870/ +EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext0 +EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext1 +EXECUTE REMOVEDIR Contents/Resources/8/87/871/ +EXECUTE REMOVEDIR Contents/Resources/8/87/ +EXECUTE REMOVEFILE Contents/Resources/8/86/86text0 +EXECUTE REMOVEFILE Contents/Resources/8/86/86text1 +EXECUTE REMOVEDIR Contents/Resources/8/86/ +EXECUTE REMOVEDIR Contents/Resources/8/85/ +EXECUTE REMOVEDIR Contents/Resources/8/85/ +EXECUTE REMOVEDIR Contents/Resources/8/84/ +EXECUTE REMOVEDIR Contents/Resources/8/84/ +EXECUTE REMOVEDIR Contents/Resources/8/83/ +EXECUTE REMOVEDIR Contents/Resources/8/82/ +EXECUTE REMOVEDIR Contents/Resources/8/81/ +EXECUTE REMOVEDIR Contents/Resources/8/80/ +EXECUTE REMOVEDIR Contents/Resources/8/80/ +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtest.exe +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext0 +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext1 +EXECUTE REMOVEDIR Contents/Resources/7/70/ +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtest.exe +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext0 +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext1 +EXECUTE REMOVEDIR Contents/Resources/7/71/ +EXECUTE REMOVEFILE Contents/Resources/7/7text0 +EXECUTE REMOVEFILE Contents/Resources/7/7text1 +EXECUTE REMOVEDIR Contents/Resources/7/ +EXECUTE REMOVEDIR Contents/Resources/6/ +EXECUTE REMOVEFILE Contents/Resources/5/5text1 +EXECUTE REMOVEFILE Contents/Resources/5/5text0 +EXECUTE REMOVEFILE Contents/Resources/5/5test.exe +EXECUTE REMOVEFILE Contents/Resources/5/5text0 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEFILE Contents/Resources/5/5text1 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEDIR Contents/Resources/5/ +EXECUTE REMOVEFILE Contents/Resources/4/4text1 +EXECUTE REMOVEFILE Contents/Resources/4/4text0 +EXECUTE REMOVEDIR Contents/Resources/4/ +EXECUTE REMOVEFILE Contents/Resources/3/3text1 +EXECUTE REMOVEFILE Contents/Resources/3/3text0 +EXECUTE REMOVEDIR Contents/Resources/1/10/ +EXECUTE REMOVEDIR Contents/Resources/1/ +FINISH ADD Contents/Resources/searchplugins/searchpluginstext0 +FINISH PATCH Contents/Resources/searchplugins/searchpluginspng1.png +FINISH PATCH Contents/Resources/searchplugins/searchpluginspng0.png +FINISH ADD Contents/Resources/precomplete +FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +FINISH PATCH Contents/Resources/0/0exe0.exe +FINISH ADD Contents/Resources/0/00/00text0 +FINISH PATCH Contents/Resources/0/00/00png0.png +FINISH PATCH Contents/MacOS/exe0.exe +FINISH ADD Contents/Resources/2/20/20text0 +FINISH ADD Contents/Resources/2/20/20png0.png +FINISH ADD Contents/Resources/0/00/00text2 +FINISH REMOVEFILE Contents/Resources/1/10/10text0 +FINISH REMOVEFILE Contents/Resources/0/00/00text1 +FINISH REMOVEDIR Contents/Resources/9/99/ +FINISH REMOVEDIR Contents/Resources/9/99/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/98/ +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0 +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1 +FINISH REMOVEDIR Contents/Resources/9/97/970/ +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0 +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1 +FINISH REMOVEDIR Contents/Resources/9/97/971/ +FINISH REMOVEDIR Contents/Resources/9/97/ +FINISH REMOVEFILE Contents/Resources/9/96/96text0 +FINISH REMOVEFILE Contents/Resources/9/96/96text1 +FINISH REMOVEDIR Contents/Resources/9/96/ +FINISH REMOVEDIR Contents/Resources/9/95/ +FINISH REMOVEDIR Contents/Resources/9/95/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/94/ +FINISH REMOVEDIR Contents/Resources/9/94/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/93/ +FINISH REMOVEDIR Contents/Resources/9/92/ +removing directory: Contents/Resources/9/92/, rv: 0 +FINISH REMOVEDIR Contents/Resources/9/91/ +removing directory: Contents/Resources/9/91/, rv: 0 +FINISH REMOVEDIR Contents/Resources/9/90/ +FINISH REMOVEDIR Contents/Resources/9/90/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/89/ +FINISH REMOVEDIR Contents/Resources/8/89/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/88/ +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0 +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1 +FINISH REMOVEDIR Contents/Resources/8/87/870/ +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0 +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1 +FINISH REMOVEDIR Contents/Resources/8/87/871/ +FINISH REMOVEDIR Contents/Resources/8/87/ +FINISH REMOVEFILE Contents/Resources/8/86/86text0 +FINISH REMOVEFILE Contents/Resources/8/86/86text1 +FINISH REMOVEDIR Contents/Resources/8/86/ +FINISH REMOVEDIR Contents/Resources/8/85/ +FINISH REMOVEDIR Contents/Resources/8/85/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/84/ +FINISH REMOVEDIR Contents/Resources/8/84/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/83/ +FINISH REMOVEDIR Contents/Resources/8/82/ +removing directory: Contents/Resources/8/82/, rv: 0 +FINISH REMOVEDIR Contents/Resources/8/81/ +removing directory: Contents/Resources/8/81/, rv: 0 +FINISH REMOVEDIR Contents/Resources/8/80/ +FINISH REMOVEDIR Contents/Resources/8/80/ +directory no longer exists; skipping +FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe +FINISH REMOVEFILE Contents/Resources/7/70/7xtext0 +FINISH REMOVEFILE Contents/Resources/7/70/7xtext1 +FINISH REMOVEDIR Contents/Resources/7/70/ +FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe +FINISH REMOVEFILE Contents/Resources/7/71/7xtext0 +FINISH REMOVEFILE Contents/Resources/7/71/7xtext1 +FINISH REMOVEDIR Contents/Resources/7/71/ +FINISH REMOVEFILE Contents/Resources/7/7text0 +FINISH REMOVEFILE Contents/Resources/7/7text1 +FINISH REMOVEDIR Contents/Resources/7/ +FINISH REMOVEDIR Contents/Resources/6/ +FINISH REMOVEFILE Contents/Resources/5/5text1 +FINISH REMOVEFILE Contents/Resources/5/5text0 +FINISH REMOVEFILE Contents/Resources/5/5test.exe +FINISH REMOVEDIR Contents/Resources/5/ +FINISH REMOVEFILE Contents/Resources/4/4text1 +FINISH REMOVEFILE Contents/Resources/4/4text0 +FINISH REMOVEDIR Contents/Resources/4/ +FINISH REMOVEFILE Contents/Resources/3/3text1 +FINISH REMOVEFILE Contents/Resources/3/3text0 +FINISH REMOVEDIR Contents/Resources/1/10/ +FINISH REMOVEDIR Contents/Resources/1/ +succeeded +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/partial_log_success_win b/toolkit/mozapps/update/tests/data/partial_log_success_win new file mode 100644 index 000000000..1f5c4b3b4 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_log_success_win @@ -0,0 +1,279 @@ +UPDATE TYPE partial +PREPARE ADD searchplugins/searchpluginstext0 +PREPARE PATCH searchplugins/searchpluginspng1.png +PREPARE PATCH searchplugins/searchpluginspng0.png +PREPARE ADD precomplete +PREPARE PATCH exe0.exe +PREPARE ADD distribution/extensions/extensions1/extensions1text0 +PREPARE PATCH distribution/extensions/extensions1/extensions1png1.png +PREPARE PATCH distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD distribution/extensions/extensions0/extensions0text0 +PREPARE PATCH distribution/extensions/extensions0/extensions0png1.png +PREPARE PATCH distribution/extensions/extensions0/extensions0png0.png +PREPARE PATCH 0/0exe0.exe +PREPARE ADD 0/00/00text0 +PREPARE PATCH 0/00/00png0.png +PREPARE ADD 2/20/20text0 +PREPARE ADD 2/20/20png0.png +PREPARE ADD 0/00/00text2 +PREPARE REMOVEFILE 1/10/10text0 +PREPARE REMOVEFILE 0/00/00text1 +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/98/ +PREPARE REMOVEFILE 9/97/970/97xtext0 +PREPARE REMOVEFILE 9/97/970/97xtext1 +PREPARE REMOVEDIR 9/97/970/ +PREPARE REMOVEFILE 9/97/971/97xtext0 +PREPARE REMOVEFILE 9/97/971/97xtext1 +PREPARE REMOVEDIR 9/97/971/ +PREPARE REMOVEDIR 9/97/ +PREPARE REMOVEFILE 9/96/96text0 +PREPARE REMOVEFILE 9/96/96text1 +PREPARE REMOVEDIR 9/96/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/93/ +PREPARE REMOVEDIR 9/92/ +PREPARE REMOVEDIR 9/91/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/88/ +PREPARE REMOVEFILE 8/87/870/87xtext0 +PREPARE REMOVEFILE 8/87/870/87xtext1 +PREPARE REMOVEDIR 8/87/870/ +PREPARE REMOVEFILE 8/87/871/87xtext0 +PREPARE REMOVEFILE 8/87/871/87xtext1 +PREPARE REMOVEDIR 8/87/871/ +PREPARE REMOVEDIR 8/87/ +PREPARE REMOVEFILE 8/86/86text0 +PREPARE REMOVEFILE 8/86/86text1 +PREPARE REMOVEDIR 8/86/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/83/ +PREPARE REMOVEDIR 8/82/ +PREPARE REMOVEDIR 8/81/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEFILE 7/70/7xtest.exe +PREPARE REMOVEFILE 7/70/7xtext0 +PREPARE REMOVEFILE 7/70/7xtext1 +PREPARE REMOVEDIR 7/70/ +PREPARE REMOVEFILE 7/71/7xtest.exe +PREPARE REMOVEFILE 7/71/7xtext0 +PREPARE REMOVEFILE 7/71/7xtext1 +PREPARE REMOVEDIR 7/71/ +PREPARE REMOVEFILE 7/7text0 +PREPARE REMOVEFILE 7/7text1 +PREPARE REMOVEDIR 7/ +PREPARE REMOVEDIR 6/ +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5test.exe +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEDIR 5/ +PREPARE REMOVEFILE 4/4text1 +PREPARE REMOVEFILE 4/4text0 +PREPARE REMOVEDIR 4/ +PREPARE REMOVEFILE 3/3text1 +PREPARE REMOVEFILE 3/3text0 +PREPARE REMOVEDIR 1/10/ +PREPARE REMOVEDIR 1/ +EXECUTE ADD searchplugins/searchpluginstext0 +EXECUTE PATCH searchplugins/searchpluginspng1.png +EXECUTE PATCH searchplugins/searchpluginspng0.png +EXECUTE ADD precomplete +EXECUTE PATCH exe0.exe +EXECUTE ADD distribution/extensions/extensions1/extensions1text0 +EXECUTE PATCH distribution/extensions/extensions1/extensions1png1.png +EXECUTE PATCH distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD distribution/extensions/extensions0/extensions0text0 +EXECUTE PATCH distribution/extensions/extensions0/extensions0png1.png +EXECUTE PATCH distribution/extensions/extensions0/extensions0png0.png +EXECUTE PATCH 0/0exe0.exe +EXECUTE ADD 0/00/00text0 +EXECUTE PATCH 0/00/00png0.png +EXECUTE ADD 2/20/20text0 +EXECUTE ADD 2/20/20png0.png +EXECUTE ADD 0/00/00text2 +EXECUTE REMOVEFILE 1/10/10text0 +EXECUTE REMOVEFILE 0/00/00text1 +EXECUTE REMOVEDIR 9/99/ +EXECUTE REMOVEDIR 9/99/ +EXECUTE REMOVEDIR 9/98/ +EXECUTE REMOVEFILE 9/97/970/97xtext0 +EXECUTE REMOVEFILE 9/97/970/97xtext1 +EXECUTE REMOVEDIR 9/97/970/ +EXECUTE REMOVEFILE 9/97/971/97xtext0 +EXECUTE REMOVEFILE 9/97/971/97xtext1 +EXECUTE REMOVEDIR 9/97/971/ +EXECUTE REMOVEDIR 9/97/ +EXECUTE REMOVEFILE 9/96/96text0 +EXECUTE REMOVEFILE 9/96/96text1 +EXECUTE REMOVEDIR 9/96/ +EXECUTE REMOVEDIR 9/95/ +EXECUTE REMOVEDIR 9/95/ +EXECUTE REMOVEDIR 9/94/ +EXECUTE REMOVEDIR 9/94/ +EXECUTE REMOVEDIR 9/93/ +EXECUTE REMOVEDIR 9/92/ +EXECUTE REMOVEDIR 9/91/ +EXECUTE REMOVEDIR 9/90/ +EXECUTE REMOVEDIR 9/90/ +EXECUTE REMOVEDIR 8/89/ +EXECUTE REMOVEDIR 8/89/ +EXECUTE REMOVEDIR 8/88/ +EXECUTE REMOVEFILE 8/87/870/87xtext0 +EXECUTE REMOVEFILE 8/87/870/87xtext1 +EXECUTE REMOVEDIR 8/87/870/ +EXECUTE REMOVEFILE 8/87/871/87xtext0 +EXECUTE REMOVEFILE 8/87/871/87xtext1 +EXECUTE REMOVEDIR 8/87/871/ +EXECUTE REMOVEDIR 8/87/ +EXECUTE REMOVEFILE 8/86/86text0 +EXECUTE REMOVEFILE 8/86/86text1 +EXECUTE REMOVEDIR 8/86/ +EXECUTE REMOVEDIR 8/85/ +EXECUTE REMOVEDIR 8/85/ +EXECUTE REMOVEDIR 8/84/ +EXECUTE REMOVEDIR 8/84/ +EXECUTE REMOVEDIR 8/83/ +EXECUTE REMOVEDIR 8/82/ +EXECUTE REMOVEDIR 8/81/ +EXECUTE REMOVEDIR 8/80/ +EXECUTE REMOVEDIR 8/80/ +EXECUTE REMOVEFILE 7/70/7xtest.exe +EXECUTE REMOVEFILE 7/70/7xtext0 +EXECUTE REMOVEFILE 7/70/7xtext1 +EXECUTE REMOVEDIR 7/70/ +EXECUTE REMOVEFILE 7/71/7xtest.exe +EXECUTE REMOVEFILE 7/71/7xtext0 +EXECUTE REMOVEFILE 7/71/7xtext1 +EXECUTE REMOVEDIR 7/71/ +EXECUTE REMOVEFILE 7/7text0 +EXECUTE REMOVEFILE 7/7text1 +EXECUTE REMOVEDIR 7/ +EXECUTE REMOVEDIR 6/ +EXECUTE REMOVEFILE 5/5text1 +EXECUTE REMOVEFILE 5/5text0 +EXECUTE REMOVEFILE 5/5test.exe +EXECUTE REMOVEFILE 5/5text0 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEFILE 5/5text1 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEDIR 5/ +EXECUTE REMOVEFILE 4/4text1 +EXECUTE REMOVEFILE 4/4text0 +EXECUTE REMOVEDIR 4/ +EXECUTE REMOVEFILE 3/3text1 +EXECUTE REMOVEFILE 3/3text0 +EXECUTE REMOVEDIR 1/10/ +EXECUTE REMOVEDIR 1/ +FINISH ADD searchplugins/searchpluginstext0 +FINISH PATCH searchplugins/searchpluginspng1.png +FINISH PATCH searchplugins/searchpluginspng0.png +FINISH ADD precomplete +FINISH PATCH exe0.exe +FINISH ADD distribution/extensions/extensions1/extensions1text0 +FINISH PATCH distribution/extensions/extensions1/extensions1png1.png +FINISH PATCH distribution/extensions/extensions1/extensions1png0.png +FINISH ADD distribution/extensions/extensions0/extensions0text0 +FINISH PATCH distribution/extensions/extensions0/extensions0png1.png +FINISH PATCH distribution/extensions/extensions0/extensions0png0.png +FINISH PATCH 0/0exe0.exe +FINISH ADD 0/00/00text0 +FINISH PATCH 0/00/00png0.png +FINISH ADD 2/20/20text0 +FINISH ADD 2/20/20png0.png +FINISH ADD 0/00/00text2 +FINISH REMOVEFILE 1/10/10text0 +FINISH REMOVEFILE 0/00/00text1 +FINISH REMOVEDIR 9/99/ +FINISH REMOVEDIR 9/99/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/98/ +FINISH REMOVEFILE 9/97/970/97xtext0 +FINISH REMOVEFILE 9/97/970/97xtext1 +FINISH REMOVEDIR 9/97/970/ +FINISH REMOVEFILE 9/97/971/97xtext0 +FINISH REMOVEFILE 9/97/971/97xtext1 +FINISH REMOVEDIR 9/97/971/ +FINISH REMOVEDIR 9/97/ +FINISH REMOVEFILE 9/96/96text0 +FINISH REMOVEFILE 9/96/96text1 +FINISH REMOVEDIR 9/96/ +FINISH REMOVEDIR 9/95/ +FINISH REMOVEDIR 9/95/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/94/ +FINISH REMOVEDIR 9/94/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/93/ +FINISH REMOVEDIR 9/92/ +removing directory: 9/92/, rv: 0 +FINISH REMOVEDIR 9/91/ +removing directory: 9/91/, rv: 0 +FINISH REMOVEDIR 9/90/ +FINISH REMOVEDIR 9/90/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/89/ +FINISH REMOVEDIR 8/89/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/88/ +FINISH REMOVEFILE 8/87/870/87xtext0 +FINISH REMOVEFILE 8/87/870/87xtext1 +FINISH REMOVEDIR 8/87/870/ +FINISH REMOVEFILE 8/87/871/87xtext0 +FINISH REMOVEFILE 8/87/871/87xtext1 +FINISH REMOVEDIR 8/87/871/ +FINISH REMOVEDIR 8/87/ +FINISH REMOVEFILE 8/86/86text0 +FINISH REMOVEFILE 8/86/86text1 +FINISH REMOVEDIR 8/86/ +FINISH REMOVEDIR 8/85/ +FINISH REMOVEDIR 8/85/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/84/ +FINISH REMOVEDIR 8/84/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/83/ +FINISH REMOVEDIR 8/82/ +removing directory: 8/82/, rv: 0 +FINISH REMOVEDIR 8/81/ +removing directory: 8/81/, rv: 0 +FINISH REMOVEDIR 8/80/ +FINISH REMOVEDIR 8/80/ +directory no longer exists; skipping +FINISH REMOVEFILE 7/70/7xtest.exe +FINISH REMOVEFILE 7/70/7xtext0 +FINISH REMOVEFILE 7/70/7xtext1 +FINISH REMOVEDIR 7/70/ +FINISH REMOVEFILE 7/71/7xtest.exe +FINISH REMOVEFILE 7/71/7xtext0 +FINISH REMOVEFILE 7/71/7xtext1 +FINISH REMOVEDIR 7/71/ +FINISH REMOVEFILE 7/7text0 +FINISH REMOVEFILE 7/7text1 +FINISH REMOVEDIR 7/ +FINISH REMOVEDIR 6/ +FINISH REMOVEFILE 5/5text1 +FINISH REMOVEFILE 5/5text0 +FINISH REMOVEFILE 5/5test.exe +FINISH REMOVEDIR 5/ +FINISH REMOVEFILE 4/4text1 +FINISH REMOVEFILE 4/4text0 +FINISH REMOVEDIR 4/ +FINISH REMOVEFILE 3/3text1 +FINISH REMOVEFILE 3/3text0 +FINISH REMOVEDIR 1/10/ +FINISH REMOVEDIR 1/ +succeeded +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/partial_mac.mar b/toolkit/mozapps/update/tests/data/partial_mac.mar Binary files differnew file mode 100644 index 000000000..5a702ed4a --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_mac.mar diff --git a/toolkit/mozapps/update/tests/data/partial_precomplete b/toolkit/mozapps/update/tests/data/partial_precomplete new file mode 100644 index 000000000..3ec201463 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_precomplete @@ -0,0 +1,19 @@ +remove "searchplugins/searchpluginstext0" +remove "searchplugins/searchpluginspng1.png" +remove "searchplugins/searchpluginspng0.png" +remove "removed-files" +remove "precomplete" +remove "exe0.exe" +remove "2/20/20text0" +remove "2/20/20png0.png" +remove "0/0exe0.exe" +remove "0/00/00text2" +remove "0/00/00text0" +remove "0/00/00png0.png" +rmdir "searchplugins/" +rmdir "defaults/pref/" +rmdir "defaults/" +rmdir "2/20/" +rmdir "2/" +rmdir "0/00/" +rmdir "0/" diff --git a/toolkit/mozapps/update/tests/data/partial_precomplete_mac b/toolkit/mozapps/update/tests/data/partial_precomplete_mac new file mode 100644 index 000000000..c65b6e4e3 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_precomplete_mac @@ -0,0 +1,22 @@ +remove "Contents/Resources/searchplugins/searchpluginstext0" +remove "Contents/Resources/searchplugins/searchpluginspng1.png" +remove "Contents/Resources/searchplugins/searchpluginspng0.png" +remove "Contents/Resources/removed-files" +remove "Contents/Resources/precomplete" +remove "Contents/Resources/2/20/20text0" +remove "Contents/Resources/2/20/20png0.png" +remove "Contents/Resources/0/0exe0.exe" +remove "Contents/Resources/0/00/00text2" +remove "Contents/Resources/0/00/00text0" +remove "Contents/Resources/0/00/00png0.png" +remove "Contents/MacOS/exe0.exe" +rmdir "Contents/Resources/searchplugins/" +rmdir "Contents/Resources/defaults/pref/" +rmdir "Contents/Resources/defaults/" +rmdir "Contents/Resources/2/20/" +rmdir "Contents/Resources/2/" +rmdir "Contents/Resources/0/00/" +rmdir "Contents/Resources/0/" +rmdir "Contents/Resources/" +rmdir "Contents/MacOS/" +rmdir "Contents/" diff --git a/toolkit/mozapps/update/tests/data/partial_removed-files b/toolkit/mozapps/update/tests/data/partial_removed-files new file mode 100644 index 000000000..881311b82 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_removed-files @@ -0,0 +1,41 @@ +a/b/text0 +a/b/text1 +a/b/3/3text0 +a/b/3/3text1 +a/b/4/4exe0.exe +a/b/4/4text0 +a/b/4/4text1 +a/b/4/ +a/b/5/5text0 +a/b/5/5text1 +a/b/5/* +a/b/6/ +a/b/7/* +a/b/8/80/ +a/b/8/81/ +a/b/8/82/ +a/b/8/83/ +a/b/8/84/ +a/b/8/85/* +a/b/8/86/* +a/b/8/87/* +a/b/8/88/* +a/b/8/89/* +a/b/8/80/ +a/b/8/84/* +a/b/8/85/* +a/b/8/89/ +a/b/9/90/ +a/b/9/91/ +a/b/9/92/ +a/b/9/93/ +a/b/9/94/ +a/b/9/95/* +a/b/9/96/* +a/b/9/97/* +a/b/9/98/* +a/b/9/99/* +a/b/9/90/ +a/b/9/94/* +a/b/9/95/* +a/b/9/99/ diff --git a/toolkit/mozapps/update/tests/data/partial_removed-files_mac b/toolkit/mozapps/update/tests/data/partial_removed-files_mac new file mode 100644 index 000000000..955dc5b34 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_removed-files_mac @@ -0,0 +1,41 @@ +Contents/Resources/text0 +Contents/Resources/text1 +Contents/Resources/3/3text0 +Contents/Resources/3/3text1 +Contents/Resources/4/exe0.exe +Contents/Resources/4/4text0 +Contents/Resources/4/4text1 +Contents/Resources/4/ +Contents/Resources/5/5text0 +Contents/Resources/5/5text1 +Contents/Resources/5/* +Contents/Resources/6/ +Contents/Resources/7/* +Contents/Resources/8/80/ +Contents/Resources/8/81/ +Contents/Resources/8/82/ +Contents/Resources/8/83/ +Contents/Resources/8/84/ +Contents/Resources/8/85/* +Contents/Resources/8/86/* +Contents/Resources/8/87/* +Contents/Resources/8/88/* +Contents/Resources/8/89/* +Contents/Resources/8/80/ +Contents/Resources/8/84/* +Contents/Resources/8/85/* +Contents/Resources/8/89/ +Contents/Resources/9/90/ +Contents/Resources/9/91/ +Contents/Resources/9/92/ +Contents/Resources/9/93/ +Contents/Resources/9/94/ +Contents/Resources/9/95/* +Contents/Resources/9/96/* +Contents/Resources/9/97/* +Contents/Resources/9/98/* +Contents/Resources/9/99/* +Contents/Resources/9/90/ +Contents/Resources/9/94/* +Contents/Resources/9/95/* +Contents/Resources/9/99/ diff --git a/toolkit/mozapps/update/tests/data/partial_update_manifest b/toolkit/mozapps/update/tests/data/partial_update_manifest new file mode 100644 index 000000000..8d4e60ed2 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_update_manifest @@ -0,0 +1,63 @@ +type "partial" +add "precomplete" +add "a/b/searchplugins/searchpluginstext0" +patch-if "a/b/searchplugins/searchpluginspng1.png" "a/b/searchplugins/searchpluginspng1.png.patch" "a/b/searchplugins/searchpluginspng1.png" +patch-if "a/b/searchplugins/searchpluginspng0.png" "a/b/searchplugins/searchpluginspng0.png.patch" "a/b/searchplugins/searchpluginspng0.png" +add-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1text0" +patch-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1png1.png.patch" "a/b/extensions/extensions1/extensions1png1.png" +patch-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1png0.png.patch" "a/b/extensions/extensions1/extensions1png0.png" +add-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0text0" +patch-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0png1.png.patch" "a/b/extensions/extensions0/extensions0png1.png" +patch-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0png0.png.patch" "a/b/extensions/extensions0/extensions0png0.png" +patch "a/b/exe0.exe.patch" "a/b/exe0.exe" +patch "a/b/0/0exe0.exe.patch" "a/b/0/0exe0.exe" +add "a/b/0/00/00text0" +patch "a/b/0/00/00png0.png.patch" "a/b/0/00/00png0.png" +add "a/b/2/20/20text0" +add "a/b/2/20/20png0.png" +add "a/b/0/00/00text2" +remove "a/b/1/10/10text0" +remove "a/b/0/00/00text1" +remove "a/b/text1" +remove "a/b/text0" +rmrfdir "a/b/9/99/" +rmdir "a/b/9/99/" +rmrfdir "a/b/9/98/" +rmrfdir "a/b/9/97/" +rmrfdir "a/b/9/96/" +rmrfdir "a/b/9/95/" +rmrfdir "a/b/9/95/" +rmrfdir "a/b/9/94/" +rmdir "a/b/9/94/" +rmdir "a/b/9/93/" +rmdir "a/b/9/92/" +rmdir "a/b/9/91/" +rmdir "a/b/9/90/" +rmdir "a/b/9/90/" +rmrfdir "a/b/8/89/" +rmdir "a/b/8/89/" +rmrfdir "a/b/8/88/" +rmrfdir "a/b/8/87/" +rmrfdir "a/b/8/86/" +rmrfdir "a/b/8/85/" +rmrfdir "a/b/8/85/" +rmrfdir "a/b/8/84/" +rmdir "a/b/8/84/" +rmdir "a/b/8/83/" +rmdir "a/b/8/82/" +rmdir "a/b/8/81/" +rmdir "a/b/8/80/" +rmdir "a/b/8/80/" +rmrfdir "a/b/7/" +rmdir "a/b/6/" +remove "a/b/5/5text1" +remove "a/b/5/5text0" +rmrfdir "a/b/5/" +remove "a/b/4/4text1" +remove "a/b/4/4text0" +remove "a/b/4/4exe0.exe" +rmdir "a/b/4/" +remove "a/b/3/3text1" +remove "a/b/3/3text0" +rmdir "a/b/1/10/" +rmdir "a/b/1/" diff --git a/toolkit/mozapps/update/tests/data/replace_log_success b/toolkit/mozapps/update/tests/data/replace_log_success new file mode 100644 index 000000000..323f1db41 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/replace_log_success @@ -0,0 +1,6 @@ +Performing a replace request +rename_file: proceeding to rename the directory +rename_file: proceeding to rename the directory +Now, remove the tmpDir +succeeded +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/shared.js b/toolkit/mozapps/update/tests/data/shared.js new file mode 100644 index 000000000..e9a10da06 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/shared.js @@ -0,0 +1,632 @@ +/* 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/. */ + +/* Shared code for xpcshell and mochitests-chrome */ +/* eslint-disable no-undef */ + +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const PREF_APP_UPDATE_AUTO = "app.update.auto"; +const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors"; +const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors"; +const PREF_APP_UPDATE_CHANNEL = "app.update.channel"; +const PREF_APP_UPDATE_DOWNLOADBACKGROUNDINTERVAL = "app.update.download.backgroundInterval"; +const PREF_APP_UPDATE_ENABLED = "app.update.enabled"; +const PREF_APP_UPDATE_IDLETIME = "app.update.idletime"; +const PREF_APP_UPDATE_LOG = "app.update.log"; +const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported"; +const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime"; +const PREF_APP_UPDATE_RETRYTIMEOUT = "app.update.socket.retryTimeout"; +const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled"; +const PREF_APP_UPDATE_SILENT = "app.update.silent"; +const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors"; +const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled"; +const PREF_APP_UPDATE_URL = "app.update.url"; +const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details"; + +const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never."; + +const PREFBRANCH_APP_PARTNER = "app.partner."; +const PREF_DISTRIBUTION_ID = "distribution.id"; +const PREF_DISTRIBUTION_VERSION = "distribution.version"; +const PREF_TOOLKIT_TELEMETRY_ENABLED = "toolkit.telemetry.enabled"; + +const NS_APP_PROFILE_DIR_STARTUP = "ProfDS"; +const NS_APP_USER_PROFILE_50_DIR = "ProfD"; +const NS_GRE_DIR = "GreD"; +const NS_GRE_BIN_DIR = "GreBinD"; +const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD"; +const XRE_EXECUTABLE_FILE = "XREExeF"; +const XRE_UPDATE_ROOT_DIR = "UpdRootD"; + +const DIR_PATCH = "0"; +const DIR_TOBEDELETED = "tobedeleted"; +const DIR_UPDATES = "updates"; +const DIR_UPDATED = IS_MACOSX ? "Updated.app" : "updated"; + +const FILE_ACTIVE_UPDATE_XML = "active-update.xml"; +const FILE_APPLICATION_INI = "application.ini"; +const FILE_BACKUP_UPDATE_LOG = "backup-update.log"; +const FILE_LAST_UPDATE_LOG = "last-update.log"; +const FILE_UPDATE_SETTINGS_INI = "update-settings.ini"; +const FILE_UPDATE_SETTINGS_INI_BAK = "update-settings.ini.bak"; +const FILE_UPDATER_INI = "updater.ini"; +const FILE_UPDATES_XML = "updates.xml"; +const FILE_UPDATE_LOG = "update.log"; +const FILE_UPDATE_MAR = "update.mar"; +const FILE_UPDATE_STATUS = "update.status"; +const FILE_UPDATE_TEST = "update.test"; +const FILE_UPDATE_VERSION = "update.version"; + +const UPDATE_SETTINGS_CONTENTS = "[Settings]\n" + + "ACCEPTED_MAR_CHANNEL_IDS=xpcshell-test\n"; + +const PR_RDWR = 0x04; +const PR_CREATE_FILE = 0x08; +const PR_TRUNCATE = 0x20; + +const DEFAULT_UPDATE_VERSION = "999999.0"; + +var gChannel; + +/* import-globals-from ../data/sharedUpdateXML.js */ +Services.scriptloader.loadSubScript(DATA_URI_SPEC + "sharedUpdateXML.js", this); + +const PERMS_FILE = FileUtils.PERMS_FILE; +const PERMS_DIRECTORY = FileUtils.PERMS_DIRECTORY; + +const MODE_WRONLY = FileUtils.MODE_WRONLY; +const MODE_CREATE = FileUtils.MODE_CREATE; +const MODE_APPEND = FileUtils.MODE_APPEND; +const MODE_TRUNCATE = FileUtils.MODE_TRUNCATE; + +const URI_UPDATES_PROPERTIES = "chrome://mozapps/locale/update/updates.properties"; +const gUpdateBundle = Services.strings.createBundle(URI_UPDATES_PROPERTIES); + +XPCOMUtils.defineLazyGetter(this, "gAUS", function test_gAUS() { + return Cc["@mozilla.org/updates/update-service;1"]. + getService(Ci.nsIApplicationUpdateService). + QueryInterface(Ci.nsITimerCallback). + QueryInterface(Ci.nsIObserver). + QueryInterface(Ci.nsIUpdateCheckListener); +}); + +XPCOMUtils.defineLazyServiceGetter(this, "gUpdateManager", + "@mozilla.org/updates/update-manager;1", + "nsIUpdateManager"); + +XPCOMUtils.defineLazyGetter(this, "gUpdateChecker", function test_gUC() { + return Cc["@mozilla.org/updates/update-checker;1"]. + createInstance(Ci.nsIUpdateChecker); +}); + +XPCOMUtils.defineLazyGetter(this, "gUP", function test_gUP() { + return Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); +}); + +XPCOMUtils.defineLazyGetter(this, "gDefaultPrefBranch", function test_gDPB() { + return Services.prefs.getDefaultBranch(null); +}); + +XPCOMUtils.defineLazyGetter(this, "gPrefRoot", function test_gPR() { + return Services.prefs.getBranch(null); +}); + +XPCOMUtils.defineLazyServiceGetter(this, "gEnv", + "@mozilla.org/process/environment;1", + "nsIEnvironment"); + +XPCOMUtils.defineLazyGetter(this, "gZipW", function test_gZipW() { + return Cc["@mozilla.org/zipwriter;1"]. + createInstance(Ci.nsIZipWriter); +}); + +/* Triggers post-update processing */ +function testPostUpdateProcessing() { + gAUS.observe(null, "test-post-update-processing", ""); +} + +/* Initializes the update service stub */ +function initUpdateServiceStub() { + Cc["@mozilla.org/updates/update-service-stub;1"]. + createInstance(Ci.nsISupports); +} + +/* Reloads the update metadata from disk */ +function reloadUpdateManagerData() { + gUpdateManager.QueryInterface(Ci.nsIObserver). + observe(null, "um-reload-update-data", ""); +} + +const observer = { + observe: function(aSubject, aTopic, aData) { + if (aTopic == "nsPref:changed" && aData == PREF_APP_UPDATE_CHANNEL) { + let channel = gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL); + if (channel != gChannel) { + debugDump("Changing channel from " + channel + " to " + gChannel); + gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, gChannel); + } + } + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) +}; + +/** + * Sets the app.update.channel preference. + * + * @param aChannel + * The update channel. + */ +function setUpdateChannel(aChannel) { + gChannel = aChannel; + debugDump("setting default pref " + PREF_APP_UPDATE_CHANNEL + " to " + gChannel); + gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, gChannel); + gPrefRoot.addObserver(PREF_APP_UPDATE_CHANNEL, observer, false); +} + +/** + * Sets the app.update.url default preference. + * + * @param aURL + * The update url. If not specified 'URL_HOST + "/update.xml"' will be + * used. + */ +function setUpdateURL(aURL) { + let url = aURL ? aURL : URL_HOST + "/update.xml"; + debugDump("setting " + PREF_APP_UPDATE_URL + " to " + url); + gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_URL, url); +} + +/** + * Returns either the active or regular update database XML file. + * + * @param isActiveUpdate + * If true this will return the active-update.xml otherwise it will + * return the updates.xml file. + */ +function getUpdatesXMLFile(aIsActiveUpdate) { + let file = getUpdatesRootDir(); + file.append(aIsActiveUpdate ? FILE_ACTIVE_UPDATE_XML : FILE_UPDATES_XML); + return file; +} + +/** + * Writes the updates specified to either the active-update.xml or the + * updates.xml. + * + * @param aContent + * The updates represented as a string to write to the XML file. + * @param isActiveUpdate + * If true this will write to the active-update.xml otherwise it will + * write to the updates.xml file. + */ +function writeUpdatesToXMLFile(aContent, aIsActiveUpdate) { + writeFile(getUpdatesXMLFile(aIsActiveUpdate), aContent); +} + +/** + * Writes the current update operation/state to a file in the patch + * directory, indicating to the patching system that operations need + * to be performed. + * + * @param aStatus + * The status value to write. + */ +function writeStatusFile(aStatus) { + let file = getUpdatesPatchDir(); + file.append(FILE_UPDATE_STATUS); + writeFile(file, aStatus + "\n"); +} + +/** + * Writes the current update version to a file in the patch directory, + * indicating to the patching system the version of the update. + * + * @param aVersion + * The version value to write. + */ +function writeVersionFile(aVersion) { + let file = getUpdatesPatchDir(); + file.append(FILE_UPDATE_VERSION); + writeFile(file, aVersion + "\n"); +} + +/** + * Gets the root directory for the updates directory. + * + * @return nsIFile for the updates root directory. + */ +function getUpdatesRootDir() { + return Services.dirsvc.get(XRE_UPDATE_ROOT_DIR, Ci.nsIFile); +} + +/** + * Gets the updates directory. + * + * @return nsIFile for the updates directory. + */ +function getUpdatesDir() { + let dir = getUpdatesRootDir(); + dir.append(DIR_UPDATES); + return dir; +} + +/** + * Gets the directory for update patches. + * + * @return nsIFile for the updates directory. + */ +function getUpdatesPatchDir() { + let dir = getUpdatesDir(); + dir.append(DIR_PATCH); + return dir; +} + +/** + * Writes text to a file. This will replace existing text if the file exists + * and create the file if it doesn't exist. + * + * @param aFile + * The file to write to. Will be created if it doesn't exist. + * @param aText + * The text to write to the file. If there is existing text it will be + * replaced. + */ +function writeFile(aFile, aText) { + let fos = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + if (!aFile.exists()) { + aFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE); + } + fos.init(aFile, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, PERMS_FILE, 0); + fos.write(aText, aText.length); + fos.close(); +} + +/** + * Reads the current update operation/state in the status file in the patch + * directory including the error code if it is present. + * + * @return The status value. + */ +function readStatusFile() { + let file = getUpdatesPatchDir(); + file.append(FILE_UPDATE_STATUS); + + if (!file.exists()) { + debugDump("update status file does not exists! Path: " + file.path); + return STATE_NONE; + } + + return readFile(file).split("\n")[0]; +} + +/** + * Reads the current update operation/state in the status file in the patch + * directory without the error code if it is present. + * + * @return The state value. + */ +function readStatusState() { + return readStatusFile().split(": ")[0]; +} + +/** + * Reads the current update operation/state in the status file in the patch + * directory with the error code. + * + * @return The state value. + */ +function readStatusFailedCode() { + return readStatusFile().split(": ")[1]; +} + +/** + * Reads text from a file and returns the string. + * + * @param aFile + * The file to read from. + * @return The string of text read from the file. + */ +function readFile(aFile) { + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + if (!aFile.exists()) { + return null; + } + // Specifying -1 for ioFlags will open the file with the default of PR_RDONLY. + // Specifying -1 for perm will open the file with the default of 0. + fis.init(aFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF); + let sis = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + sis.init(fis); + let text = sis.read(sis.available()); + sis.close(); + return text; +} + +/** + * Reads the binary contents of a file and returns it as a string. + * + * @param aFile + * The file to read from. + * @return The contents of the file as a string. + */ +function readFileBytes(aFile) { + debugDump("attempting to read file, path: " + aFile.path); + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + // Specifying -1 for ioFlags will open the file with the default of PR_RDONLY. + // Specifying -1 for perm will open the file with the default of 0. + fis.init(aFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF); + let bis = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let data = []; + let count = fis.available(); + while (count > 0) { + let bytes = bis.readByteArray(Math.min(65535, count)); + data.push(String.fromCharCode.apply(null, bytes)); + count -= bytes.length; + if (bytes.length == 0) { + throw "Nothing read from input stream!"; + } + } + data.join(''); + fis.close(); + return data.toString(); +} + +/* Returns human readable status text from the updates.properties bundle */ +function getStatusText(aErrCode) { + return getString("check_error-" + aErrCode); +} + +/* Returns a string from the updates.properties bundle */ +function getString(aName) { + try { + return gUpdateBundle.GetStringFromName(aName); + } catch (e) { + } + return null; +} + +/** + * Gets the file extension for an nsIFile. + * + * @param aFile + * The file to get the file extension for. + * @return The file extension. + */ +function getFileExtension(aFile) { + return Services.io.newFileURI(aFile).QueryInterface(Ci.nsIURL). + fileExtension; +} + +/** + * Removes the updates.xml file, active-update.xml file, and all files and + * sub-directories in the updates directory except for the "0" sub-directory. + * This prevents some tests from failing due to files being left behind when the + * tests are interrupted. + */ +function removeUpdateDirsAndFiles() { + let file = getUpdatesXMLFile(true); + try { + if (file.exists()) { + file.remove(false); + } + } catch (e) { + logTestInfo("Unable to remove file. Path: " + file.path + + ", Exception: " + e); + } + + file = getUpdatesXMLFile(false); + try { + if (file.exists()) { + file.remove(false); + } + } catch (e) { + logTestInfo("Unable to remove file. Path: " + file.path + + ", Exception: " + e); + } + + // This fails sporadically on Mac OS X so wrap it in a try catch + let updatesDir = getUpdatesDir(); + try { + cleanUpdatesDir(updatesDir); + } catch (e) { + logTestInfo("Unable to remove files / directories from directory. Path: " + + updatesDir.path + ", Exception: " + e); + } +} + +/** + * Removes all files and sub-directories in the updates directory except for + * the "0" sub-directory. + * + * @param aDir + * nsIFile for the directory to be deleted. + */ +function cleanUpdatesDir(aDir) { + if (!aDir.exists()) { + return; + } + + let dirEntries = aDir.directoryEntries; + while (dirEntries.hasMoreElements()) { + let entry = dirEntries.getNext().QueryInterface(Ci.nsIFile); + + if (entry.isDirectory()) { + if (entry.leafName == DIR_PATCH && entry.parent.leafName == DIR_UPDATES) { + cleanUpdatesDir(entry); + entry.permissions = PERMS_DIRECTORY; + } else { + try { + entry.remove(true); + return; + } catch (e) { + } + cleanUpdatesDir(entry); + entry.permissions = PERMS_DIRECTORY; + try { + entry.remove(true); + } catch (e) { + logTestInfo("cleanUpdatesDir: unable to remove directory. Path: " + + entry.path + ", Exception: " + e); + throw (e); + } + } + } else { + entry.permissions = PERMS_FILE; + try { + entry.remove(false); + } catch (e) { + logTestInfo("cleanUpdatesDir: unable to remove file. Path: " + + entry.path + ", Exception: " + e); + throw (e); + } + } + } +} + +/** + * Deletes a directory and its children. First it tries nsIFile::Remove(true). + * If that fails it will fall back to recursing, setting the appropriate + * permissions, and deleting the current entry. + * + * @param aDir + * nsIFile for the directory to be deleted. + */ +function removeDirRecursive(aDir) { + if (!aDir.exists()) { + return; + } + + try { + debugDump("attempting to remove directory. Path: " + aDir.path); + aDir.remove(true); + return; + } catch (e) { + logTestInfo("non-fatal error removing directory. Exception: " + e); + } + + let dirEntries = aDir.directoryEntries; + while (dirEntries.hasMoreElements()) { + let entry = dirEntries.getNext().QueryInterface(Ci.nsIFile); + + if (entry.isDirectory()) { + removeDirRecursive(entry); + } else { + entry.permissions = PERMS_FILE; + try { + debugDump("attempting to remove file. Path: " + entry.path); + entry.remove(false); + } catch (e) { + logTestInfo("error removing file. Exception: " + e); + throw (e); + } + } + } + + aDir.permissions = PERMS_DIRECTORY; + try { + debugDump("attempting to remove directory. Path: " + aDir.path); + aDir.remove(true); + } catch (e) { + logTestInfo("error removing directory. Exception: " + e); + throw (e); + } +} + +/** + * Returns the directory for the currently running process. This is used to + * clean up after the tests and to locate the active-update.xml and updates.xml + * files. + * + * @return nsIFile for the current process directory. + */ +function getCurrentProcessDir() { + return Services.dirsvc.get(NS_XPCOM_CURRENT_PROCESS_DIR, Ci.nsIFile); +} + +/** + * Gets the application base directory. + * + * @return nsIFile object for the application base directory. + */ +function getAppBaseDir() { + return Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).parent; +} + +/** + * Returns the Gecko Runtime Engine directory where files other than executable + * binaries are located. On Mac OS X this will be <bundle>/Contents/Resources/ + * and the installation directory on all other platforms. + * + * @return nsIFile for the Gecko Runtime Engine directory. + */ +function getGREDir() { + return Services.dirsvc.get(NS_GRE_DIR, Ci.nsIFile); +} + +/** + * Returns the Gecko Runtime Engine Binary directory where the executable + * binaries are located such as the updater binary (Windows and Linux) or + * updater package (Mac OS X). On Mac OS X this will be + * <bundle>/Contents/MacOS/ and the installation directory on all other + * platforms. + * + * @return nsIFile for the Gecko Runtime Engine Binary directory. + */ +function getGREBinDir() { + return Services.dirsvc.get(NS_GRE_BIN_DIR, Ci.nsIFile); +} + +/** + * Logs TEST-INFO messages. + * + * @param aText + * The text to log. + * @param aCaller (optional) + * An optional Components.stack.caller. If not specified + * Components.stack.caller will be used. + */ +function logTestInfo(aText, aCaller) { + let caller = aCaller ? aCaller : Components.stack.caller; + let now = new Date(); + let hh = now.getHours(); + let mm = now.getMinutes(); + let ss = now.getSeconds(); + let ms = now.getMilliseconds(); + let time = (hh < 10 ? "0" + hh : hh) + ":" + + (mm < 10 ? "0" + mm : mm) + ":" + + (ss < 10 ? "0" + ss : ss) + ":"; + if (ms < 10) { + time += "00"; + } else if (ms < 100) { + time += "0"; + } + time += ms; + let msg = time + " | TEST-INFO | " + caller.filename + " | [" + caller.name + + " : " + caller.lineNumber + "] " + aText; + LOG_FUNCTION(msg); +} + +/** + * Logs TEST-INFO messages when DEBUG_AUS_TEST evaluates to true. + * + * @param aText + * The text to log. + * @param aCaller (optional) + * An optional Components.stack.caller. If not specified + * Components.stack.caller will be used. + */ +function debugDump(aText, aCaller) { + if (DEBUG_AUS_TEST) { + let caller = aCaller ? aCaller : Components.stack.caller; + logTestInfo(aText, caller); + } +} diff --git a/toolkit/mozapps/update/tests/data/sharedUpdateXML.js b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js new file mode 100644 index 000000000..3aa01eff4 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js @@ -0,0 +1,364 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Helper functions for creating xml strings used by application update tests. + * + * !IMPORTANT - This file contains everything needed (along with dependencies) + * by the updates.sjs file used by the mochitest-chrome tests. Since xpcshell + * used by the http server is launched with -v 170 this file must not use + * features greater than JavaScript 1.7. + */ + +/* eslint-disable no-undef */ + +const FILE_SIMPLE_MAR = "simple.mar"; +const SIZE_SIMPLE_MAR = "1031"; +const MD5_HASH_SIMPLE_MAR = "1f8c038577bb6845d94ccec4999113ee"; +const SHA1_HASH_SIMPLE_MAR = "5d49a672c87f10f31d7e326349564a11272a028b"; +const SHA256_HASH_SIMPLE_MAR = "1aabbed5b1dd6e16e139afc5b43d479e254e0c26" + + "3c8fb9249c0a1bb93071c5fb"; +const SHA384_HASH_SIMPLE_MAR = "26615014ea034af32ef5651492d5f493f5a7a1a48522e" + + "d24c366442a5ec21d5ef02e23fb58d79729b8ca2f9541" + + "99dd53"; +const SHA512_HASH_SIMPLE_MAR = "922e5ae22081795f6e8d65a3c508715c9a314054179a8" + + "bbfe5f50dc23919ad89888291bc0a07586ab17dd0304a" + + "b5347473601127571c66f61f5080348e05c36b"; + +const STATE_NONE = "null"; +const STATE_DOWNLOADING = "downloading"; +const STATE_PENDING = "pending"; +const STATE_PENDING_SVC = "pending-service"; +const STATE_APPLYING = "applying"; +const STATE_APPLIED = "applied"; +const STATE_APPLIED_SVC = "applied-service"; +const STATE_SUCCEEDED = "succeeded"; +const STATE_DOWNLOAD_FAILED = "download-failed"; +const STATE_FAILED = "failed"; + +const LOADSOURCE_ERROR_WRONG_SIZE = 2; +const CRC_ERROR = 4; +const READ_ERROR = 6; +const WRITE_ERROR = 7; +const MAR_CHANNEL_MISMATCH_ERROR = 22; +const VERSION_DOWNGRADE_ERROR = 23; +const SERVICE_COULD_NOT_COPY_UPDATER = 49; +const SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR = 52; +const SERVICE_INVALID_APPLYTO_DIR_ERROR = 54; +const SERVICE_INVALID_INSTALL_DIR_PATH_ERROR = 55; +const SERVICE_INVALID_WORKING_DIR_PATH_ERROR = 56; +const INVALID_APPLYTO_DIR_STAGED_ERROR = 72; +const INVALID_APPLYTO_DIR_ERROR = 74; +const INVALID_INSTALL_DIR_PATH_ERROR = 75; +const INVALID_WORKING_DIR_PATH_ERROR = 76; +const INVALID_CALLBACK_PATH_ERROR = 77; +const INVALID_CALLBACK_DIR_ERROR = 78; + +const STATE_FAILED_DELIMETER = ": "; + +const STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE = + STATE_FAILED + STATE_FAILED_DELIMETER + LOADSOURCE_ERROR_WRONG_SIZE; +const STATE_FAILED_CRC_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + CRC_ERROR; +const STATE_FAILED_READ_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + READ_ERROR; +const STATE_FAILED_WRITE_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + WRITE_ERROR; +const STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + MAR_CHANNEL_MISMATCH_ERROR; +const STATE_FAILED_VERSION_DOWNGRADE_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + VERSION_DOWNGRADE_ERROR; +const STATE_FAILED_SERVICE_COULD_NOT_COPY_UPDATER = + STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_COULD_NOT_COPY_UPDATER +const STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR; +const STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_APPLYTO_DIR_ERROR; +const STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_INSTALL_DIR_PATH_ERROR; +const STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_WORKING_DIR_PATH_ERROR; +const STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_STAGED_ERROR; +const STATE_FAILED_INVALID_APPLYTO_DIR_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_ERROR; +const STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_INSTALL_DIR_PATH_ERROR; +const STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_WORKING_DIR_PATH_ERROR; +const STATE_FAILED_INVALID_CALLBACK_PATH_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_CALLBACK_PATH_ERROR; +const STATE_FAILED_INVALID_CALLBACK_DIR_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_CALLBACK_DIR_ERROR; + +/** + * Constructs a string representing a remote update xml file. + * + * @param aUpdates + * The string representing the update elements. + * @return The string representing a remote update xml file. + */ +function getRemoteUpdatesXMLString(aUpdates) { + return "<?xml version=\"1.0\"?>\n" + + "<updates>\n" + + aUpdates + + "</updates>\n"; +} + +/** + * Constructs a string representing an update element for a remote update xml + * file. See getUpdateString for parameter information not provided below. + * + * @param aPatches + * String representing the application update patches. + * @return The string representing an update element for an update xml file. + */ +function getRemoteUpdateString(aPatches, aType, aName, aDisplayVersion, + aAppVersion, aBuildID, aDetailsURL, aShowPrompt, + aShowNeverForVersion, aPromptWaitTime, + aBackgroundInterval, aCustom1, aCustom2) { + return getUpdateString(aType, aName, aDisplayVersion, aAppVersion, + aBuildID, aDetailsURL, aShowPrompt, + aShowNeverForVersion, aPromptWaitTime, + aBackgroundInterval, aCustom1, aCustom2) + ">\n" + + aPatches + + " </update>\n"; +} + +/** + * Constructs a string representing a patch element for a remote update xml + * file. See getPatchString for parameter information not provided below. + * + * @return The string representing a patch element for a remote update xml file. + */ +function getRemotePatchString(aType, aURL, aHashFunction, aHashValue, aSize) { + return getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) + + "/>\n"; +} + +/** + * Constructs a string representing a local update xml file. + * + * @param aUpdates + * The string representing the update elements. + * @return The string representing a local update xml file. + */ +function getLocalUpdatesXMLString(aUpdates) { + if (!aUpdates || aUpdates == "") { + return "<updates xmlns=\"http://www.mozilla.org/2005/app-update\"/>"; + } + return ("<updates xmlns=\"http://www.mozilla.org/2005/app-update\">" + + aUpdates + + "</updates>").replace(/>\s+\n*</g, '><'); +} + +/** + * Constructs a string representing an update element for a local update xml + * file. See getUpdateString for parameter information not provided below. + * + * @param aPatches + * String representing the application update patches. + * @param aServiceURL (optional) + * The update's xml url. + * If not specified it will default to 'http://test_service/'. + * @param aIsCompleteUpdate (optional) + * The string 'true' if this update was a complete update or the string + * 'false' if this update was a partial update. + * If not specified it will default to 'true'. + * @param aChannel (optional) + * The update channel name. + * If not specified it will default to the default preference value of + * app.update.channel. + * @param aForegroundDownload (optional) + * The string 'true' if this update was manually downloaded or the + * string 'false' if this update was automatically downloaded. + * If not specified it will default to 'true'. + * @param aPreviousAppVersion (optional) + * The application version prior to applying the update. + * If not specified it will not be present. + * @return The string representing an update element for an update xml file. + */ +function getLocalUpdateString(aPatches, aType, aName, aDisplayVersion, + aAppVersion, aBuildID, aDetailsURL, aServiceURL, + aInstallDate, aStatusText, aIsCompleteUpdate, + aChannel, aForegroundDownload, aShowPrompt, + aShowNeverForVersion, aPromptWaitTime, + aBackgroundInterval, aPreviousAppVersion, + aCustom1, aCustom2) { + let serviceURL = aServiceURL ? aServiceURL : "http://test_service/"; + let installDate = aInstallDate ? aInstallDate : "1238441400314"; + let statusText = aStatusText ? aStatusText : "Install Pending"; + let isCompleteUpdate = + typeof aIsCompleteUpdate == "string" ? aIsCompleteUpdate : "true"; + let channel = aChannel ? aChannel + : gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL); + let foregroundDownload = + typeof aForegroundDownload == "string" ? aForegroundDownload : "true"; + let previousAppVersion = aPreviousAppVersion ? "previousAppVersion=\"" + + aPreviousAppVersion + "\" " + : ""; + return getUpdateString(aType, aName, aDisplayVersion, aAppVersion, aBuildID, + aDetailsURL, aShowPrompt, aShowNeverForVersion, + aPromptWaitTime, aBackgroundInterval, aCustom1, aCustom2) + + " " + + previousAppVersion + + "serviceURL=\"" + serviceURL + "\" " + + "installDate=\"" + installDate + "\" " + + "statusText=\"" + statusText + "\" " + + "isCompleteUpdate=\"" + isCompleteUpdate + "\" " + + "channel=\"" + channel + "\" " + + "foregroundDownload=\"" + foregroundDownload + "\">" + + aPatches + + " </update>"; +} + +/** + * Constructs a string representing a patch element for a local update xml file. + * See getPatchString for parameter information not provided below. + * + * @param aSelected (optional) + * Whether this patch is selected represented or not. The string 'true' + * denotes selected and the string 'false' denotes not selected. + * If not specified it will default to the string 'true'. + * @param aState (optional) + * The patch's state. + * If not specified it will default to STATE_SUCCEEDED. + * @return The string representing a patch element for a local update xml file. + */ +function getLocalPatchString(aType, aURL, aHashFunction, aHashValue, aSize, + aSelected, aState) { + let selected = typeof aSelected == "string" ? aSelected : "true"; + let state = aState ? aState : STATE_SUCCEEDED; + return getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) + " " + + "selected=\"" + selected + "\" " + + "state=\"" + state + "\"/>\n"; +} + +/** + * Constructs a string representing an update element for a remote update xml + * file. + * + * @param aType (optional) + * The update's type which should be major or minor. If not specified it + * will default to 'major'. + * @param aName (optional) + * The update's name. + * If not specified it will default to 'App Update Test'. + * @param aDisplayVersion (optional) + * The update's display version. + * If not specified it will default to 'version #' where # is the value + * of DEFAULT_UPDATE_VERSION. + * @param aAppVersion (optional) + * The update's application version. + * If not specified it will default to the value of + * DEFAULT_UPDATE_VERSION. + * @param aBuildID (optional) + * The update's build id. + * If not specified it will default to '20080811053724'. + * @param aDetailsURL (optional) + * The update's details url. + * If not specified it will default to 'http://test_details/' due to due + * to bug 470244. + * @param aShowPrompt (optional) + * Whether to show the prompt for the update when auto update is + * enabled. + * If not specified it will not be present and the update service will + * default to false. + * @param aShowNeverForVersion (optional) + * Whether to show the 'No Thanks' button in the update prompt. + * If not specified it will not be present and the update service will + * default to false. + * @param aPromptWaitTime (optional) + * Override for the app.update.promptWaitTime preference. + * @param aBackgroundInterval (optional) + * Override for the app.update.download.backgroundInterval preference. + * @param aCustom1 (optional) + * A custom attribute name and attribute value to add to the xml. + * Example: custom1_attribute="custom1 value" + * If not specified it will not be present. + * @param aCustom2 (optional) + * A custom attribute name and attribute value to add to the xml. + * Example: custom2_attribute="custom2 value" + * If not specified it will not be present. + * @return The string representing an update element for an update xml file. + */ +function getUpdateString(aType, aName, aDisplayVersion, aAppVersion, aBuildID, + aDetailsURL, aShowPrompt, aShowNeverForVersion, + aPromptWaitTime, aBackgroundInterval, aCustom1, + aCustom2) { + let type = aType ? aType : "major"; + let name = aName ? aName : "App Update Test"; + let displayVersion = aDisplayVersion ? "displayVersion=\"" + + aDisplayVersion + "\" " + : ""; + let appVersion = "appVersion=\"" + + (aAppVersion ? aAppVersion : DEFAULT_UPDATE_VERSION) + + "\" "; + let buildID = aBuildID ? aBuildID : "20080811053724"; + // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244 +// let detailsURL = aDetailsURL ? "detailsURL=\"" + aDetailsURL + "\" " : ""; + let detailsURL = "detailsURL=\"" + + (aDetailsURL ? aDetailsURL + : "http://test_details/") + "\" "; + let showPrompt = aShowPrompt ? "showPrompt=\"" + aShowPrompt + "\" " : ""; + let showNeverForVersion = aShowNeverForVersion ? "showNeverForVersion=\"" + + aShowNeverForVersion + "\" " + : ""; + let promptWaitTime = aPromptWaitTime ? "promptWaitTime=\"" + aPromptWaitTime + + "\" " + : ""; + let backgroundInterval = aBackgroundInterval ? "backgroundInterval=\"" + + aBackgroundInterval + "\" " + : ""; + let custom1 = aCustom1 ? aCustom1 + " " : ""; + let custom2 = aCustom2 ? aCustom2 + " " : ""; + return " <update type=\"" + type + "\" " + + "name=\"" + name + "\" " + + displayVersion + + appVersion + + detailsURL + + showPrompt + + showNeverForVersion + + promptWaitTime + + backgroundInterval + + custom1 + + custom2 + + "buildID=\"" + buildID + "\""; +} + +/** + * Constructs a string representing a patch element for an update xml file. + * + * @param aType (optional) + * The patch's type which should be complete or partial. + * If not specified it will default to 'complete'. + * @param aURL (optional) + * The patch's url to the mar file. + * If not specified it will default to the value of: + * gURLData + FILE_SIMPLE_MAR + * @param aHashFunction (optional) + * The patch's hash function used to verify the mar file. + * If not specified it will default to 'MD5'. + * @param aHashValue (optional) + * The patch's hash value used to verify the mar file. + * If not specified it will default to the value of MD5_HASH_SIMPLE_MAR + * which is the MD5 hash value for the file specified by FILE_SIMPLE_MAR. + * @param aSize (optional) + * The patch's file size for the mar file. + * If not specified it will default to the file size for FILE_SIMPLE_MAR + * specified by SIZE_SIMPLE_MAR. + * @return The string representing a patch element for an update xml file. + */ +function getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) { + let type = aType ? aType : "complete"; + let url = aURL ? aURL : gURLData + FILE_SIMPLE_MAR; + let hashFunction = aHashFunction ? aHashFunction : "MD5"; + let hashValue = aHashValue ? aHashValue : MD5_HASH_SIMPLE_MAR; + let size = aSize ? aSize : SIZE_SIMPLE_MAR; + return " <patch type=\"" + type + "\" " + + "URL=\"" + url + "\" " + + "hashFunction=\"" + hashFunction + "\" " + + "hashValue=\"" + hashValue + "\" " + + "size=\"" + size + "\""; +} diff --git a/toolkit/mozapps/update/tests/data/simple.mar b/toolkit/mozapps/update/tests/data/simple.mar Binary files differnew file mode 100644 index 000000000..b2ccbd8d2 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/simple.mar diff --git a/toolkit/mozapps/update/tests/data/wrong_product_channel.mar b/toolkit/mozapps/update/tests/data/wrong_product_channel.mar Binary files differnew file mode 100644 index 000000000..1e39cc214 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/wrong_product_channel.mar diff --git a/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js b/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js new file mode 100644 index 000000000..e5f0fce58 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js @@ -0,0 +1,53 @@ +/* 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/. */ + +/* Preprocessed constants used by xpcshell tests */ + +const INSTALL_LOCALE = "@AB_CD@"; +const MOZ_APP_NAME = "@MOZ_APP_NAME@"; +const BIN_SUFFIX = "@BIN_SUFFIX@"; + +// MOZ_APP_VENDOR is optional. +#ifdef MOZ_APP_VENDOR +const MOZ_APP_VENDOR = "@MOZ_APP_VENDOR@"; +#else +const MOZ_APP_VENDOR = ""; +#endif + +// MOZ_APP_BASENAME is not optional for tests. +const MOZ_APP_BASENAME = "@MOZ_APP_BASENAME@"; +const APP_BIN_SUFFIX = "@BIN_SUFFIX@"; + +const APP_INFO_NAME = "XPCShell"; +const APP_INFO_VENDOR = "Mozilla"; + +#ifdef XP_WIN +const IS_WIN = true; +#else +const IS_WIN = false; +#endif + +#ifdef XP_MACOSX +const IS_MACOSX = true; +#else +const IS_MACOSX = false; +#endif + +#ifdef XP_UNIX +const IS_UNIX = true; +#else +const IS_UNIX = false; +#endif + +#ifdef MOZ_VERIFY_MAR_SIGNATURE +const MOZ_VERIFY_MAR_SIGNATURE = true; +#else +const MOZ_VERIFY_MAR_SIGNATURE = false; +#endif + +#ifdef DISABLE_UPDATER_AUTHENTICODE_CHECK + const IS_AUTHENTICODE_CHECK_ENABLED = false; +#else + const IS_AUTHENTICODE_CHECK_ENABLED = true; +#endif diff --git a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js new file mode 100644 index 000000000..ada08f0ae --- /dev/null +++ b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js @@ -0,0 +1,4047 @@ +/* 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/. */ + +/** + * Test log warnings that happen before the test has started + * "Couldn't get the user appdata directory. Crash events may not be produced." + * in nsExceptionHandler.cpp (possibly bug 619104) + * + * Test log warnings that happen after the test has finished + * "OOPDeinit() without successful OOPInit()" in nsExceptionHandler.cpp + * (bug 619104) + * "XPCOM objects created/destroyed from static ctor/dtor" in nsTraceRefcnt.cpp + * (possibly bug 457479) + * + * Other warnings printed to the test logs + * "site security information will not be persisted" in + * nsSiteSecurityService.cpp and the error in nsSystemInfo.cpp preceding this + * error are due to not having a profile when running some of the xpcshell + * tests. Since most xpcshell tests also log these errors these tests don't + * call do_get_profile unless necessary for the test. + * The "This method is lossy. Use GetCanonicalPath !" warning on Windows in + * nsLocalFileWin.cpp is from the call to GetNSSProfilePath in + * nsNSSComponent.cpp due to it using GetNativeCanonicalPath. + * "!mMainThread" in nsThreadManager.cpp are due to using timers and it might be + * possible to fix some or all of these in the test itself. + * "NS_FAILED(rv)" in nsThreadUtils.cpp are due to using timers and it might be + * possible to fix some or all of these in the test itself. + */ + +'use strict'; +/* eslint-disable no-undef */ + +const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr, + utils: Cu } = Components; + +/* global INSTALL_LOCALE, MOZ_APP_NAME, BIN_SUFFIX, MOZ_APP_VENDOR */ +/* global MOZ_APP_BASENAME, APP_BIN_SUFFIX, APP_INFO_NAME, APP_INFO_VENDOR */ +/* global IS_WIN, IS_MACOSX, IS_UNIX, MOZ_VERIFY_MAR_SIGNATURE */ +/* global IS_AUTHENTICODE_CHECK_ENABLED */ +load("../data/xpcshellConstantsPP.js"); + +function getLogSuffix() { + if (IS_WIN) { + return "_win"; + } + if (IS_MACOSX) { + return "_mac"; + } + return "_linux"; +} + +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/ctypes.jsm", this); + +const DIR_MACOS = IS_MACOSX ? "Contents/MacOS/" : ""; +const DIR_RESOURCES = IS_MACOSX ? "Contents/Resources/" : ""; +const TEST_FILE_SUFFIX = IS_MACOSX ? "_mac" : ""; +const FILE_COMPLETE_MAR = "complete" + TEST_FILE_SUFFIX + ".mar"; +const FILE_PARTIAL_MAR = "partial" + TEST_FILE_SUFFIX + ".mar"; +const FILE_COMPLETE_PRECOMPLETE = "complete_precomplete" + TEST_FILE_SUFFIX; +const FILE_PARTIAL_PRECOMPLETE = "partial_precomplete" + TEST_FILE_SUFFIX; +const FILE_COMPLETE_REMOVEDFILES = "complete_removed-files" + TEST_FILE_SUFFIX; +const FILE_PARTIAL_REMOVEDFILES = "partial_removed-files" + TEST_FILE_SUFFIX; +const FILE_UPDATE_IN_PROGRESS_LOCK = "updated.update_in_progress.lock"; +const COMPARE_LOG_SUFFIX = getLogSuffix(); +const LOG_COMPLETE_SUCCESS = "complete_log_success" + COMPARE_LOG_SUFFIX; +const LOG_PARTIAL_SUCCESS = "partial_log_success" + COMPARE_LOG_SUFFIX; +const LOG_PARTIAL_FAILURE = "partial_log_failure" + COMPARE_LOG_SUFFIX; +const LOG_REPLACE_SUCCESS = "replace_log_success"; + +const USE_EXECV = IS_UNIX && !IS_MACOSX; + +const URL_HOST = "http://localhost"; + +const FILE_APP_BIN = MOZ_APP_NAME + APP_BIN_SUFFIX; +const FILE_COMPLETE_EXE = "complete.exe"; +const FILE_HELPER_BIN = "TestAUSHelper" + BIN_SUFFIX; +const FILE_MAINTENANCE_SERVICE_BIN = "maintenanceservice.exe"; +const FILE_MAINTENANCE_SERVICE_INSTALLER_BIN = "maintenanceservice_installer.exe"; +const FILE_OLD_VERSION_MAR = "old_version.mar"; +const FILE_PARTIAL_EXE = "partial.exe"; +const FILE_UPDATER_BIN = "updater" + BIN_SUFFIX; +const FILE_WRONG_CHANNEL_MAR = "wrong_product_channel.mar"; + +const PERFORMING_STAGED_UPDATE = "Performing a staged update"; +const CALL_QUIT = "calling QuitProgressUI"; +const REMOVE_OLD_DIST_DIR = "removing old distribution directory"; +const MOVE_OLD_DIST_DIR = "Moving old distribution directory to new location"; +const ERR_UPDATE_IN_PROGRESS = "Update already in progress! Exiting"; +const ERR_RENAME_FILE = "rename_file: failed to rename file"; +const ERR_ENSURE_COPY = "ensure_copy: failed to copy the file"; +const ERR_UNABLE_OPEN_DEST = "unable to open destination file"; +const ERR_BACKUP_DISCARD = "backup_discard: unable to remove"; +const ERR_MOVE_DESTDIR_7 = "Moving destDir to tmpDir failed, err: 7"; +const ERR_BACKUP_CREATE_7 = "backup_create failed: 7"; +const ERR_LOADSOURCEFILE_FAILED = "LoadSourceFile failed"; + +const LOG_SVC_SUCCESSFUL_LAUNCH = "Process was started... waiting on result."; + +// Typical end of a message when calling assert +const MSG_SHOULD_EQUAL = " should equal the expected value"; +const MSG_SHOULD_EXIST = "the file or directory should exist"; +const MSG_SHOULD_NOT_EXIST = "the file or directory should not exist"; + +// All we care about is that the last modified time has changed so that Mac OS +// X Launch Services invalidates its cache so the test allows up to one minute +// difference in the last modified time. +const MAC_MAX_TIME_DIFFERENCE = 60000; + +// How many of do_execute_soon calls to wait before the test is aborted. +const MAX_TIMEOUT_RUNS = 20000; + +// Time in seconds the helper application should sleep before exiting. The +// helper can also be made to exit by writing |finish| to its input file. +const HELPER_SLEEP_TIMEOUT = 180; + +// Maximum number of milliseconds the process that is launched can run before +// the test will try to kill it. +const APP_TIMER_TIMEOUT = 120000; + +// How many of do_timeout calls using FILE_IN_USE_TIMEOUT_MS to wait before the +// test is aborted. +const FILE_IN_USE_MAX_TIMEOUT_RUNS = 60; +const FILE_IN_USE_TIMEOUT_MS = 1000; + +const PIPE_TO_NULL = IS_WIN ? ">nul" : "> /dev/null 2>&1"; + +const LOG_FUNCTION = do_print; + +const gHTTPHandlerPath = "updates.xml"; + +// This default value will be overridden when using the http server. +var gURLData = URL_HOST + "/"; + +var gTestID; + +var gTestserver; + +var gRegisteredServiceCleanup; + +var gCheckFunc; +var gResponseBody; +var gResponseStatusCode = 200; +var gRequestURL; +var gUpdateCount; +var gUpdates; +var gStatusCode; +var gStatusText; +var gStatusResult; + +var gProcess; +var gAppTimer; +var gHandle; + +var gGREDirOrig; +var gGREBinDirOrig; +var gAppDirOrig; + +// Variables are used instead of contants so tests can override these values if +// necessary. +var gCallbackBinFile = "callback_app" + BIN_SUFFIX; +var gCallbackArgs = ["./", "callback.log", "Test Arg 2", "Test Arg 3"]; +var gPostUpdateBinFile = "postup_app" + BIN_SUFFIX; +var gSvcOriginalLogContents; +var gUseTestAppDir = true; +// Some update staging failures can remove the update. This allows tests to +// specify that the status file and the active update should not be checked +// after an update is staged. +var gStagingRemovedUpdate = false; + +var gTimeoutRuns = 0; +var gFileInUseTimeoutRuns = 0; + +// Environment related globals +var gShouldResetEnv = undefined; +var gAddedEnvXRENoWindowsCrashDialog = false; +var gEnvXPCOMDebugBreak; +var gEnvXPCOMMemLeakLog; +var gEnvDyldLibraryPath; +var gEnvLdLibraryPath; +var gASanOptions; + +// Set to true to log additional information for debugging. To log additional +// information for an individual test set DEBUG_AUS_TEST to true in the test's +// run_test function. +var DEBUG_AUS_TEST = true; + +const DATA_URI_SPEC = Services.io.newFileURI(do_get_file("../data", false)).spec; +/* import-globals-from ../data/shared.js */ +Services.scriptloader.loadSubScript(DATA_URI_SPEC + "shared.js", this); + +var gTestFiles = []; +var gTestDirs = []; + +// Common files for both successful and failed updates. +var gTestFilesCommon = [ + { + description: "Should never change", + fileName: FILE_UPDATE_SETTINGS_INI, + relPathDir: DIR_RESOURCES, + originalContents: UPDATE_SETTINGS_CONTENTS, + compareContents: UPDATE_SETTINGS_CONTENTS, + originalFile: null, + compareFile: null, + originalPerms: 0o767, + comparePerms: 0o767 + }, { + description: "Should never change", + fileName: "channel-prefs.js", + relPathDir: DIR_RESOURCES + "defaults/pref/", + originalContents: "ShouldNotBeReplaced\n", + compareContents: "ShouldNotBeReplaced\n", + originalFile: null, + compareFile: null, + originalPerms: 0o767, + comparePerms: 0o767 + }]; + + // Files for a complete successful update. This can be used for a complete + // failed update by calling setTestFilesAndDirsForFailure. +var gTestFilesCompleteSuccess = [ + { + description: "Added by update.manifest (add)", + fileName: "precomplete", + relPathDir: DIR_RESOURCES, + originalContents: null, + compareContents: null, + originalFile: FILE_PARTIAL_PRECOMPLETE, + compareFile: FILE_COMPLETE_PRECOMPLETE, + originalPerms: 0o666, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "searchpluginstext0", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: "ToBeReplacedWithFromComplete\n", + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: 0o775, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "searchpluginspng1.png", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "complete.png", + originalPerms: null, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "searchpluginspng0.png", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: null, + compareContents: null, + originalFile: "partial.png", + compareFile: "complete.png", + originalPerms: 0o666, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "removed-files", + relPathDir: DIR_RESOURCES, + originalContents: null, + compareContents: null, + originalFile: FILE_PARTIAL_REMOVEDFILES, + compareFile: FILE_COMPLETE_REMOVEDFILES, + originalPerms: 0o666, + comparePerms: 0o644 + }, { + description: "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions1text0", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: 0o644 + }, { + description: "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions1png1.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: null, + originalFile: "partial.png", + compareFile: "complete.png", + originalPerms: 0o666, + comparePerms: 0o644 + }, { + description: "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions1png0.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "complete.png", + originalPerms: null, + comparePerms: 0o644 + }, { + description: "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions0text0", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: "ToBeReplacedWithFromComplete\n", + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: 0o644 + }, { + description: "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions0png1.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "complete.png", + originalPerms: null, + comparePerms: 0o644 + }, { + description: "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions0png0.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "complete.png", + originalPerms: null, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "exe0.exe", + relPathDir: DIR_MACOS, + originalContents: null, + compareContents: null, + originalFile: FILE_HELPER_BIN, + compareFile: FILE_COMPLETE_EXE, + originalPerms: 0o777, + comparePerms: 0o755 + }, { + description: "Added by update.manifest (add)", + fileName: "10text0", + relPathDir: DIR_RESOURCES + "1/10/", + originalContents: "ToBeReplacedWithFromComplete\n", + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: 0o767, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "0exe0.exe", + relPathDir: DIR_RESOURCES + "0/", + originalContents: null, + compareContents: null, + originalFile: FILE_HELPER_BIN, + compareFile: FILE_COMPLETE_EXE, + originalPerms: 0o777, + comparePerms: 0o755 + }, { + description: "Added by update.manifest (add)", + fileName: "00text1", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: "ToBeReplacedWithFromComplete\n", + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: 0o677, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "00text0", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: "ToBeReplacedWithFromComplete\n", + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: 0o775, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "00png0.png", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "complete.png", + originalPerms: 0o776, + comparePerms: 0o644 + }, { + description: "Removed by precomplete (remove)", + fileName: "20text0", + relPathDir: DIR_RESOURCES + "2/20/", + originalContents: "ToBeDeleted\n", + compareContents: null, + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: null + }, { + description: "Removed by precomplete (remove)", + fileName: "20png0.png", + relPathDir: DIR_RESOURCES + "2/20/", + originalContents: "ToBeDeleted\n", + compareContents: null, + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: null + }]; + +// Concatenate the common files to the end of the array. +gTestFilesCompleteSuccess = gTestFilesCompleteSuccess.concat(gTestFilesCommon); + +// Files for a partial successful update. This can be used for a partial failed +// update by calling setTestFilesAndDirsForFailure. +var gTestFilesPartialSuccess = [ + { + description: "Added by update.manifest (add)", + fileName: "precomplete", + relPathDir: DIR_RESOURCES, + originalContents: null, + compareContents: null, + originalFile: FILE_COMPLETE_PRECOMPLETE, + compareFile: FILE_PARTIAL_PRECOMPLETE, + originalPerms: 0o666, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "searchpluginstext0", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: "ToBeReplacedWithFromPartial\n", + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: 0o775, + comparePerms: 0o644 + }, { + description: "Patched by update.manifest if the file exists (patch-if)", + fileName: "searchpluginspng1.png", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o666, + comparePerms: 0o666 + }, { + description: "Patched by update.manifest if the file exists (patch-if)", + fileName: "searchpluginspng0.png", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o666, + comparePerms: 0o666 + }, { + description: "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions1text0", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: 0o644 + }, { + description: "Patched by update.manifest if the parent directory exists (patch-if)", + fileName: "extensions1png1.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o666, + comparePerms: 0o666 + }, { + description: "Patched by update.manifest if the parent directory exists (patch-if)", + fileName: "extensions1png0.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o666, + comparePerms: 0o666 + }, { + description: "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions0text0", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: "ToBeReplacedWithFromPartial\n", + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: 0o644, + comparePerms: 0o644 + }, { + description: "Patched by update.manifest if the parent directory exists (patch-if)", + fileName: "extensions0png1.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o644, + comparePerms: 0o644 + }, { + description: "Patched by update.manifest if the parent directory exists (patch-if)", + fileName: "extensions0png0.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o644, + comparePerms: 0o644 + }, { + description: "Patched by update.manifest (patch)", + fileName: "exe0.exe", + relPathDir: DIR_MACOS, + originalContents: null, + compareContents: null, + originalFile: FILE_COMPLETE_EXE, + compareFile: FILE_PARTIAL_EXE, + originalPerms: 0o755, + comparePerms: 0o755 + }, { + description: "Patched by update.manifest (patch)", + fileName: "0exe0.exe", + relPathDir: DIR_RESOURCES + "0/", + originalContents: null, + compareContents: null, + originalFile: FILE_COMPLETE_EXE, + compareFile: FILE_PARTIAL_EXE, + originalPerms: 0o755, + comparePerms: 0o755 + }, { + description: "Added by update.manifest (add)", + fileName: "00text0", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: "ToBeReplacedWithFromPartial\n", + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: 0o644, + comparePerms: 0o644 + }, { + description: "Patched by update.manifest (patch)", + fileName: "00png0.png", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o666, + comparePerms: 0o666 + }, { + description: "Added by update.manifest (add)", + fileName: "20text0", + relPathDir: DIR_RESOURCES + "2/20/", + originalContents: null, + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "20png0.png", + relPathDir: DIR_RESOURCES + "2/20/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "partial.png", + originalPerms: null, + comparePerms: 0o644 + }, { + description: "Added by update.manifest (add)", + fileName: "00text2", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: null, + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: 0o644 + }, { + description: "Removed by update.manifest (remove)", + fileName: "10text0", + relPathDir: DIR_RESOURCES + "1/10/", + originalContents: "ToBeDeleted\n", + compareContents: null, + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: null + }, { + description: "Removed by update.manifest (remove)", + fileName: "00text1", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: "ToBeDeleted\n", + compareContents: null, + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: null + }]; + +// Concatenate the common files to the end of the array. +gTestFilesPartialSuccess = gTestFilesPartialSuccess.concat(gTestFilesCommon); + +var gTestDirsCommon = [ + { + relPathDir: DIR_RESOURCES + "3/", + dirRemoved: false, + files: ["3text0", "3text1"], + filesRemoved: true + }, { + relPathDir: DIR_RESOURCES + "4/", + dirRemoved: true, + files: ["4text0", "4text1"], + filesRemoved: true + }, { + relPathDir: DIR_RESOURCES + "5/", + dirRemoved: true, + files: ["5test.exe", "5text0", "5text1"], + filesRemoved: true + }, { + relPathDir: DIR_RESOURCES + "6/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "7/", + dirRemoved: true, + files: ["7text0", "7text1"], + subDirs: ["70/", "71/"], + subDirFiles: ["7xtest.exe", "7xtext0", "7xtext1"] + }, { + relPathDir: DIR_RESOURCES + "8/", + dirRemoved: false + }, { + relPathDir: DIR_RESOURCES + "8/80/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "8/81/", + dirRemoved: false, + files: ["81text0", "81text1"] + }, { + relPathDir: DIR_RESOURCES + "8/82/", + dirRemoved: false, + subDirs: ["820/", "821/"] + }, { + relPathDir: DIR_RESOURCES + "8/83/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "8/84/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "8/85/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "8/86/", + dirRemoved: true, + files: ["86text0", "86text1"] + }, { + relPathDir: DIR_RESOURCES + "8/87/", + dirRemoved: true, + subDirs: ["870/", "871/"], + subDirFiles: ["87xtext0", "87xtext1"] + }, { + relPathDir: DIR_RESOURCES + "8/88/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "8/89/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "9/90/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "9/91/", + dirRemoved: false, + files: ["91text0", "91text1"] + }, { + relPathDir: DIR_RESOURCES + "9/92/", + dirRemoved: false, + subDirs: ["920/", "921/"] + }, { + relPathDir: DIR_RESOURCES + "9/93/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "9/94/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "9/95/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "9/96/", + dirRemoved: true, + files: ["96text0", "96text1"] + }, { + relPathDir: DIR_RESOURCES + "9/97/", + dirRemoved: true, + subDirs: ["970/", "971/"], + subDirFiles: ["97xtext0", "97xtext1"] + }, { + relPathDir: DIR_RESOURCES + "9/98/", + dirRemoved: true + }, { + relPathDir: DIR_RESOURCES + "9/99/", + dirRemoved: true + }]; + +// Directories for a complete successful update. This array can be used for a +// complete failed update by calling setTestFilesAndDirsForFailure. +var gTestDirsCompleteSuccess = [ + { + description: "Removed by precomplete (rmdir)", + relPathDir: DIR_RESOURCES + "2/20/", + dirRemoved: true + }, { + description: "Removed by precomplete (rmdir)", + relPathDir: DIR_RESOURCES + "2/", + dirRemoved: true + }]; + +// Concatenate the common files to the beginning of the array. +gTestDirsCompleteSuccess = gTestDirsCommon.concat(gTestDirsCompleteSuccess); + +// Directories for a partial successful update. This array can be used for a +// partial failed update by calling setTestFilesAndDirsForFailure. +var gTestDirsPartialSuccess = [ + { + description: "Removed by update.manifest (rmdir)", + relPathDir: DIR_RESOURCES + "1/10/", + dirRemoved: true + }, { + description: "Removed by update.manifest (rmdir)", + relPathDir: DIR_RESOURCES + "1/", + dirRemoved: true + }]; + +// Concatenate the common files to the beginning of the array. +gTestDirsPartialSuccess = gTestDirsCommon.concat(gTestDirsPartialSuccess); + +// This makes it possible to run most tests on xulrunner where the update +// channel default preference is not set. +if (MOZ_APP_NAME == "xulrunner") { + try { + gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL); + } catch (e) { + setUpdateChannel("test_channel"); + } +} + +/** + * Helper function for setting up the test environment. + */ +function setupTestCommon() { + debugDump("start - general test setup"); + + Assert.strictEqual(gTestID, undefined, + "gTestID should be 'undefined' (setupTestCommon should " + + "only be called once)"); + + let caller = Components.stack.caller; + gTestID = caller.filename.toString().split("/").pop().split(".")[0]; + + // Tests that don't work with XULRunner. + const XUL_RUNNER_INCOMPATIBLE = ["marAppApplyUpdateAppBinInUseStageSuccess_win", + "marAppApplyUpdateStageSuccess", + "marAppApplyUpdateSuccess", + "marAppApplyUpdateAppBinInUseStageSuccessSvc_win", + "marAppApplyUpdateStageSuccessSvc", + "marAppApplyUpdateSuccessSvc"]; + // Replace with Array.prototype.includes when it has stabilized. + if (MOZ_APP_NAME == "xulrunner" && + XUL_RUNNER_INCOMPATIBLE.indexOf(gTestID) != -1) { + logTestInfo("Unable to run this test on xulrunner"); + return false; + } + + if (IS_SERVICE_TEST && !shouldRunServiceTest()) { + return false; + } + + do_test_pending(); + + setDefaultPrefs(); + + // Don't attempt to show a prompt when an update finishes. + Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, true); + + gGREDirOrig = getGREDir(); + gGREBinDirOrig = getGREBinDir(); + gAppDirOrig = getAppBaseDir(); + + let applyDir = getApplyDirFile(null, true).parent; + + // Try to remove the directory used to apply updates and the updates directory + // on platforms other than Windows. Since the test hasn't ran yet and the + // directory shouldn't exist finished this is non-fatal for the test. + if (applyDir.exists()) { + debugDump("attempting to remove directory. Path: " + applyDir.path); + try { + removeDirRecursive(applyDir); + } catch (e) { + logTestInfo("non-fatal error removing directory. Path: " + + applyDir.path + ", Exception: " + e); + // When the application doesn't exit properly it can cause the test to + // fail again on the second run with an NS_ERROR_FILE_ACCESS_DENIED error + // along with no useful information in the test log. To prevent this use + // a different directory for the test when it isn't possible to remove the + // existing test directory (bug 1294196). + gTestID += "_new"; + logTestInfo("using a new directory for the test by changing gTestID " + + "since there is an existing test directory that can't be " + + "removed, gTestID: " + gTestID); + } + } + + if (IS_WIN) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, + IS_SERVICE_TEST ? true : false); + } + + // adjustGeneralPaths registers a cleanup function that calls end_test when + // it is defined as a function. + adjustGeneralPaths(); + // Logged once here instead of in the mock directory provider to lessen test + // log spam. + debugDump("Updates Directory (UpdRootD) Path: " + getMockUpdRootD().path); + + // This prevents a warning about not being able to find the greprefs.js file + // from being logged. + let grePrefsFile = getGREDir(); + if (!grePrefsFile.exists()) { + grePrefsFile.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } + grePrefsFile.append("greprefs.js"); + if (!grePrefsFile.exists()) { + grePrefsFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE); + } + + // Remove the updates directory on Windows and Mac OS X which is located + // outside of the application directory after the call to adjustGeneralPaths + // has set it up. Since the test hasn't ran yet and the directory shouldn't + // exist this is non-fatal for the test. + if (IS_WIN || IS_MACOSX) { + let updatesDir = getMockUpdRootD(); + if (updatesDir.exists()) { + debugDump("attempting to remove directory. Path: " + updatesDir.path); + try { + removeDirRecursive(updatesDir); + } catch (e) { + logTestInfo("non-fatal error removing directory. Path: " + + updatesDir.path + ", Exception: " + e); + } + } + } + + debugDump("finish - general test setup"); + return true; +} + +/** + * Nulls out the most commonly used global vars used by tests to prevent leaks + * as needed and attempts to restore the system to its original state. + */ +function cleanupTestCommon() { + debugDump("start - general test cleanup"); + + // Force the update manager to reload the update data to prevent it from + // writing the old data to the files that have just been removed. + reloadUpdateManagerData(); + + if (gChannel) { + gPrefRoot.removeObserver(PREF_APP_UPDATE_CHANNEL, observer); + } + + // Call app update's observe method passing xpcom-shutdown to test that the + // shutdown of app update runs without throwing or leaking. The observer + // method is used directly instead of calling notifyObservers so components + // outside of the scope of this test don't assert and thereby cause app update + // tests to fail. + gAUS.observe(null, "xpcom-shutdown", ""); + + gTestserver = null; + + if (IS_UNIX) { + // This will delete the launch script if it exists. + getLaunchScript(); + } + + if (IS_WIN && MOZ_APP_BASENAME) { + let appDir = getApplyDirFile(null, true); + let vendor = MOZ_APP_VENDOR ? MOZ_APP_VENDOR : "Mozilla"; + const REG_PATH = "SOFTWARE\\" + vendor + "\\" + MOZ_APP_BASENAME + + "\\TaskBarIDs"; + let key = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + try { + key.open(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, REG_PATH, + Ci.nsIWindowsRegKey.ACCESS_ALL); + if (key.hasValue(appDir.path)) { + key.removeValue(appDir.path); + } + } catch (e) { + } + try { + key.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, REG_PATH, + Ci.nsIWindowsRegKey.ACCESS_ALL); + if (key.hasValue(appDir.path)) { + key.removeValue(appDir.path); + } + } catch (e) { + } + } + + // The updates directory is located outside of the application directory and + // needs to be removed on Windows and Mac OS X. + if (IS_WIN || IS_MACOSX) { + let updatesDir = getMockUpdRootD(); + // Try to remove the directory used to apply updates. Since the test has + // already finished this is non-fatal for the test. + if (updatesDir.exists()) { + debugDump("attempting to remove directory. Path: " + updatesDir.path); + try { + removeDirRecursive(updatesDir); + } catch (e) { + logTestInfo("non-fatal error removing directory. Path: " + + updatesDir.path + ", Exception: " + e); + } + if (IS_MACOSX) { + let updatesRootDir = gUpdatesRootDir.clone(); + while (updatesRootDir.path != updatesDir.path) { + if (updatesDir.exists()) { + debugDump("attempting to remove directory. Path: " + + updatesDir.path); + try { + // Try to remove the directory without the recursive flag set + // since the top level directory has already had its contents + // removed and the parent directory might still be used by a + // different test. + updatesDir.remove(false); + } catch (e) { + logTestInfo("non-fatal error removing directory. Path: " + + updatesDir.path + ", Exception: " + e); + if (e == Cr.NS_ERROR_FILE_DIR_NOT_EMPTY) { + break; + } + } + } + updatesDir = updatesDir.parent; + } + } + } + } + + let applyDir = getApplyDirFile(null, true).parent; + + // Try to remove the directory used to apply updates. Since the test has + // already finished this is non-fatal for the test. + if (applyDir.exists()) { + debugDump("attempting to remove directory. Path: " + applyDir.path); + try { + removeDirRecursive(applyDir); + } catch (e) { + logTestInfo("non-fatal error removing directory. Path: " + + applyDir.path + ", Exception: " + e); + } + } + + resetEnvironment(); + + debugDump("finish - general test cleanup"); +} + +/** + * Helper function that calls do_test_finished that tracks whether a parallel + * run of a test passed when it runs synchronously so the log output can be + * inspected. + */ +function doTestFinish() { + if (DEBUG_AUS_TEST) { + // This prevents do_print errors from being printed by the xpcshell test + // harness due to nsUpdateService.js logging to the console when the + // app.update.log preference is true. + Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, false); + gAUS.observe(null, "nsPref:changed", PREF_APP_UPDATE_LOG); + } + do_execute_soon(do_test_finished); +} + +/** + * Sets the most commonly used preferences used by tests + */ +function setDefaultPrefs() { + Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, true); + if (DEBUG_AUS_TEST) { + // Enable Update logging + Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, true); + } else { + // Some apps set this preference to true by default + Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, false); + } + // In case telemetry is enabled for xpcshell tests. + Services.prefs.setBoolPref(PREF_TOOLKIT_TELEMETRY_ENABLED, false); +} + +/** + * Helper function for updater binary tests that sets the appropriate values + * to check for update failures. + */ +function setTestFilesAndDirsForFailure() { + gTestFiles.forEach(function STFADFF_Files(aTestFile) { + aTestFile.compareContents = aTestFile.originalContents; + aTestFile.compareFile = aTestFile.originalFile; + aTestFile.comparePerms = aTestFile.originalPerms; + }); + + gTestDirs.forEach(function STFADFF_Dirs(aTestDir) { + aTestDir.dirRemoved = false; + if (aTestDir.filesRemoved) { + aTestDir.filesRemoved = false; + } + }); +} + +/** + * Helper function for updater binary tests that prevents the distribution + * directory files from being created. + */ +function preventDistributionFiles() { + gTestFiles = gTestFiles.filter(function(aTestFile) { + return aTestFile.relPathDir.indexOf("distribution/") == -1; + }); + + gTestDirs = gTestDirs.filter(function(aTestDir) { + return aTestDir.relPathDir.indexOf("distribution/") == -1; + }); +} + +/** + * On Mac OS X this sets the last modified time for the app bundle directory to + * a date in the past to test that the last modified time is updated when an + * update has been successfully applied (bug 600098). + */ +function setAppBundleModTime() { + if (!IS_MACOSX) { + return; + } + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; +} + +/** + * On Mac OS X this checks that the last modified time for the app bundle + * directory has been updated when an update has been successfully applied + * (bug 600098). + */ +function checkAppBundleModTime() { + if (!IS_MACOSX) { + return; + } + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + Assert.ok(timeDiff < MAC_MAX_TIME_DIFFERENCE, + "the last modified time on the apply to directory should " + + "change after a successful update"); +} + +/** + * On Mac OS X and Windows this checks if the post update '.running' file exists + * to determine if the post update binary was launched. + * + * @param aShouldExist + * Whether the post update '.running' file should exist. + */ +function checkPostUpdateRunningFile(aShouldExist) { + if (!IS_WIN && !IS_MACOSX) { + return; + } + let postUpdateRunningFile = getPostUpdateFile(".running"); + if (aShouldExist) { + Assert.ok(postUpdateRunningFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(postUpdateRunningFile.path)); + } else { + Assert.ok(!postUpdateRunningFile.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(postUpdateRunningFile.path)); + } +} + +/** + * Initializes the most commonly used settings and creates an instance of the + * update service stub. + */ +function standardInit() { + createAppInfo("xpcshell@tests.mozilla.org", APP_INFO_NAME, "1.0", "2.0"); + // Initialize the update service stub component + initUpdateServiceStub(); +} + +/** + * Helper function for getting the application version from the application.ini + * file. This will look in both the GRE and the application directories for the + * application.ini file. + * + * @return The version string from the application.ini file. + */ +function getAppVersion() { + // Read the application.ini and use its application version. + let iniFile = gGREDirOrig.clone(); + iniFile.append(FILE_APPLICATION_INI); + if (!iniFile.exists()) { + iniFile = gGREBinDirOrig.clone(); + iniFile.append(FILE_APPLICATION_INI); + } + Assert.ok(iniFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(iniFile.path)); + let iniParser = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]. + getService(Ci.nsIINIParserFactory). + createINIParser(iniFile); + return iniParser.getString("App", "Version"); +} + +/** + * Helper function for getting the relative path to the directory where the + * application binary is located (e.g. <test_file_leafname>/dir.app/). + * + * Note: The dir.app subdirectory under <test_file_leafname> is needed for + * platforms other than Mac OS X so the tests can run in parallel due to + * update staging creating a lock file named moz_update_in_progress.lock in + * the parent directory of the installation directory. + * + * @return The relative path to the directory where application binary is + * located. + */ +function getApplyDirPath() { + return gTestID + "/dir.app/"; +} + +/** + * Helper function for getting the nsIFile for a file in the directory where the + * update will be applied. + * + * The files for the update are located two directories below the apply to + * directory since Mac OS X sets the last modified time for the root directory + * to the current time and if the update changes any files in the root directory + * then it wouldn't be possible to test (bug 600098). + * + * @param aRelPath (optional) + * The relative path to the file or directory to get from the root of + * the test's directory. If not specified the test's directory will be + * returned. + * @param aAllowNonexistent (optional) + * Whether the file must exist. If false or not specified the file must + * exist or the function will throw. + * @return The nsIFile for the file in the directory where the update will be + * applied. + * @throws If aAllowNonexistent is not specified or is false and the file or + * directory does not exist. + */ +function getApplyDirFile(aRelPath, aAllowNonexistent) { + let relpath = getApplyDirPath() + (aRelPath ? aRelPath : ""); + return do_get_file(relpath, aAllowNonexistent); +} + +/** + * Helper function for getting the nsIFile for a file in the directory where the + * update will be staged. + * + * The files for the update are located two directories below the stage + * directory since Mac OS X sets the last modified time for the root directory + * to the current time and if the update changes any files in the root directory + * then it wouldn't be possible to test (bug 600098). + * + * @param aRelPath (optional) + * The relative path to the file or directory to get from the root of + * the stage directory. If not specified the stage directory will be + * returned. + * @param aAllowNonexistent (optional) + * Whether the file must exist. If false or not specified the file must + * exist or the function will throw. + * @return The nsIFile for the file in the directory where the update will be + * staged. + * @throws If aAllowNonexistent is not specified or is false and the file or + * directory does not exist. + */ +function getStageDirFile(aRelPath, aAllowNonexistent) { + if (IS_MACOSX) { + let file = getMockUpdRootD(); + file.append(DIR_UPDATES); + file.append(DIR_PATCH); + file.append(DIR_UPDATED); + if (aRelPath) { + let pathParts = aRelPath.split("/"); + for (let i = 0; i < pathParts.length; i++) { + if (pathParts[i]) { + file.append(pathParts[i]); + } + } + } + if (!aAllowNonexistent) { + Assert.ok(file.exists(), + MSG_SHOULD_EXIST + getMsgPath(file.path)); + } + return file; + } + + let relpath = getApplyDirPath() + DIR_UPDATED + "/" + (aRelPath ? aRelPath : ""); + return do_get_file(relpath, aAllowNonexistent); +} + +/** + * Helper function for getting the relative path to the directory where the + * test data files are located. + * + * @return The relative path to the directory where the test data files are + * located. + */ +function getTestDirPath() { + return "../data/"; +} + +/** + * Helper function for getting the nsIFile for a file in the test data + * directory. + * + * @param aRelPath (optional) + * The relative path to the file or directory to get from the root of + * the test's data directory. If not specified the test's data + * directory will be returned. + * @param aAllowNonExists (optional) + * Whether or not to throw an error if the path exists. + * If not specified, then false is used. + * @return The nsIFile for the file in the test data directory. + * @throws If the file or directory does not exist. + */ +function getTestDirFile(aRelPath, aAllowNonExists) { + let relpath = getTestDirPath() + (aRelPath ? aRelPath : ""); + return do_get_file(relpath, !!aAllowNonExists); +} + +/** + * Helper function for getting the nsIFile for the maintenance service + * directory on Windows. + * + * @return The nsIFile for the maintenance service directory. + */ +function getMaintSvcDir() { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + const CSIDL_PROGRAM_FILES = 0x26; + const CSIDL_PROGRAM_FILESX86 = 0x2A; + // This will return an empty string on our Win XP build systems. + let maintSvcDir = getSpecialFolderDir(CSIDL_PROGRAM_FILESX86); + if (maintSvcDir) { + maintSvcDir.append("Mozilla Maintenance Service"); + debugDump("using CSIDL_PROGRAM_FILESX86 - maintenance service install " + + "directory path: " + maintSvcDir.path); + } + if (!maintSvcDir || !maintSvcDir.exists()) { + maintSvcDir = getSpecialFolderDir(CSIDL_PROGRAM_FILES); + if (maintSvcDir) { + maintSvcDir.append("Mozilla Maintenance Service"); + debugDump("using CSIDL_PROGRAM_FILES - maintenance service install " + + "directory path: " + maintSvcDir.path); + } + } + if (!maintSvcDir) { + do_throw("Unable to find the maintenance service install directory"); + } + + return maintSvcDir; +} + +/** + * Get the nsILocalFile for a Windows special folder determined by the CSIDL + * passed. + * + * @param aCSIDL + * The CSIDL for the Windows special folder. + * @return The nsILocalFile for the Windows special folder. + * @throws If called from a platform other than Windows. + */ +function getSpecialFolderDir(aCSIDL) { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + let lib = ctypes.open("shell32"); + let SHGetSpecialFolderPath = lib.declare("SHGetSpecialFolderPathW", + ctypes.winapi_abi, + ctypes.bool, /* bool(return) */ + ctypes.int32_t, /* HWND hwndOwner */ + ctypes.char16_t.ptr, /* LPTSTR lpszPath */ + ctypes.int32_t, /* int csidl */ + ctypes.bool /* BOOL fCreate */); + + let aryPath = ctypes.char16_t.array()(260); + let rv = SHGetSpecialFolderPath(0, aryPath, aCSIDL, false); + lib.close(); + + let path = aryPath.readString(); // Convert the c-string to js-string + if (!path) { + return null; + } + debugDump("SHGetSpecialFolderPath returned path: " + path); + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + dir.initWithPath(path); + return dir; +} + +XPCOMUtils.defineLazyGetter(this, "gInstallDirPathHash", function test_gIDPH() { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + // Figure out where we should check for a cached hash value + if (!MOZ_APP_BASENAME) { + return null; + } + + let vendor = MOZ_APP_VENDOR ? MOZ_APP_VENDOR : "Mozilla"; + let appDir = getApplyDirFile(null, true); + + const REG_PATH = "SOFTWARE\\" + vendor + "\\" + MOZ_APP_BASENAME + + "\\TaskBarIDs"; + let regKey = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + try { + regKey.open(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, REG_PATH, + Ci.nsIWindowsRegKey.ACCESS_ALL); + regKey.writeStringValue(appDir.path, gTestID); + return gTestID; + } catch (e) { + } + + try { + regKey.create(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, REG_PATH, + Ci.nsIWindowsRegKey.ACCESS_ALL); + regKey.writeStringValue(appDir.path, gTestID); + return gTestID; + } catch (e) { + logTestInfo("failed to create registry key. Registry Path: " + REG_PATH + + ", Key Name: " + appDir.path + ", Key Value: " + gTestID + + ", Exception " + e); + } + return null; +}); + +XPCOMUtils.defineLazyGetter(this, "gLocalAppDataDir", function test_gLADD() { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + const CSIDL_LOCAL_APPDATA = 0x1c; + return getSpecialFolderDir(CSIDL_LOCAL_APPDATA); +}); + +XPCOMUtils.defineLazyGetter(this, "gProgFilesDir", function test_gPFD() { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + const CSIDL_PROGRAM_FILES = 0x26; + return getSpecialFolderDir(CSIDL_PROGRAM_FILES); +}); + +/** + * Helper function for getting the update root directory used by the tests. This + * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir + * in nsXREDirProvider.cpp so an application will be able to find the update + * when running a test that launches the application. + */ +function getMockUpdRootD() { + if (IS_WIN) { + return getMockUpdRootDWin(); + } + + if (IS_MACOSX) { + return getMockUpdRootDMac(); + } + + return getApplyDirFile(DIR_MACOS, true); +} + +/** + * Helper function for getting the update root directory used by the tests. This + * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir + * in nsXREDirProvider.cpp so an application will be able to find the update + * when running a test that launches the application. + */ +function getMockUpdRootDWin() { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + let localAppDataDir = gLocalAppDataDir.clone(); + let progFilesDir = gProgFilesDir.clone(); + let appDir = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).parent; + + let appDirPath = appDir.path; + let relPathUpdates = ""; + if (gInstallDirPathHash && (MOZ_APP_VENDOR || MOZ_APP_BASENAME)) { + relPathUpdates += (MOZ_APP_VENDOR ? MOZ_APP_VENDOR : MOZ_APP_BASENAME) + + "\\" + DIR_UPDATES + "\\" + gInstallDirPathHash; + } + + if (!relPathUpdates && progFilesDir) { + if (appDirPath.length > progFilesDir.path.length) { + if (appDirPath.substr(0, progFilesDir.path.length) == progFilesDir.path) { + if (MOZ_APP_VENDOR && MOZ_APP_BASENAME) { + relPathUpdates += MOZ_APP_VENDOR + "\\" + MOZ_APP_BASENAME; + } else { + relPathUpdates += MOZ_APP_BASENAME; + } + relPathUpdates += appDirPath.substr(progFilesDir.path.length); + } + } + } + + if (!relPathUpdates) { + if (MOZ_APP_VENDOR && MOZ_APP_BASENAME) { + relPathUpdates += MOZ_APP_VENDOR + "\\" + MOZ_APP_BASENAME; + } else { + relPathUpdates += MOZ_APP_BASENAME; + } + relPathUpdates += "\\" + MOZ_APP_NAME; + } + + let updatesDir = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsILocalFile); + updatesDir.initWithPath(localAppDataDir.path + "\\" + relPathUpdates); + return updatesDir; +} + +XPCOMUtils.defineLazyGetter(this, "gUpdatesRootDir", function test_gURD() { + if (!IS_MACOSX) { + do_throw("Mac OS X only function called by a different platform!"); + } + + let dir = Services.dirsvc.get("ULibDir", Ci.nsILocalFile); + dir.append("Caches"); + if (MOZ_APP_VENDOR || MOZ_APP_BASENAME) { + dir.append(MOZ_APP_VENDOR ? MOZ_APP_VENDOR : MOZ_APP_BASENAME); + } else { + dir.append("Mozilla"); + } + dir.append(DIR_UPDATES); + return dir; +}); + +/** + * Helper function for getting the update root directory used by the tests. This + * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir + * in nsXREDirProvider.cpp so an application will be able to find the update + * when running a test that launches the application. + */ +function getMockUpdRootDMac() { + if (!IS_MACOSX) { + do_throw("Mac OS X only function called by a different platform!"); + } + + let appDir = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile). + parent.parent.parent; + let appDirPath = appDir.path; + appDirPath = appDirPath.substr(0, appDirPath.length - 4); + + let pathUpdates = gUpdatesRootDir.path + appDirPath; + let updatesDir = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsILocalFile); + updatesDir.initWithPath(pathUpdates); + return updatesDir; +} + +/** + * Creates an update in progress lock file in the specified directory on + * Windows. + * + * @param aDir + * The nsIFile for the directory where the lock file should be created. + */ +function createUpdateInProgressLockFile(aDir) { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + let file = aDir.clone(); + file.append(FILE_UPDATE_IN_PROGRESS_LOCK); + file.create(file.NORMAL_FILE_TYPE, 0o444); + file.QueryInterface(Ci.nsILocalFileWin); + file.fileAttributesWin |= file.WFA_READONLY; + file.fileAttributesWin &= ~file.WFA_READWRITE; + Assert.ok(file.exists(), + MSG_SHOULD_EXIST + getMsgPath(file.path)); + Assert.ok(!file.isWritable(), + "the lock file should not be writeable"); +} + +/** + * Removes an update in progress lock file in the specified directory on + * Windows. + * + * @param aDir + * The nsIFile for the directory where the lock file is located. + */ +function removeUpdateInProgressLockFile(aDir) { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + let file = aDir.clone(); + file.append(FILE_UPDATE_IN_PROGRESS_LOCK); + file.QueryInterface(Ci.nsILocalFileWin); + file.fileAttributesWin |= file.WFA_READWRITE; + file.fileAttributesWin &= ~file.WFA_READONLY; + file.remove(false); + Assert.ok(!file.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(file.path)); +} + +/** + * Gets the test updater from the test data direcory. + * + * @return nsIFIle for the test updater. + */ +function getTestUpdater() { + let updater = getTestDirFile("updater.app", true); + if (!updater.exists()) { + updater = getTestDirFile(FILE_UPDATER_BIN); + if (!updater.exists()) { + do_throw("Unable to find the updater binary!"); + } + } + Assert.ok(updater.exists(), + MSG_SHOULD_EXIST + getMsgPath(updater.path)); + return updater; +} + +/** + * Copies the test updater to the GRE binary directory and returns the nsIFile + * for the copied test updater. + * + * @return nsIFIle for the copied test updater. + */ +function copyTestUpdaterToBinDir() { + let testUpdater = getTestUpdater(); + let updater = getGREBinDir(); + updater.append(testUpdater.leafName); + if (!updater.exists()) { + testUpdater.copyToFollowingLinks(updater.parent, updater.leafName); + } + return updater; +} + +/** + * Copies the test updater to the location where it will be launched to apply an + * update and returns the nsIFile for the copied test updater. + * + * @return nsIFIle for the copied test updater. + */ +function copyTestUpdaterForRunUsingUpdater() { + if (IS_WIN) { + return copyTestUpdaterToBinDir(); + } + + let testUpdater = getTestUpdater(); + let updater = getUpdatesPatchDir(); + updater.append(testUpdater.leafName); + if (!updater.exists()) { + testUpdater.copyToFollowingLinks(updater.parent, updater.leafName); + } + + if (IS_MACOSX) { + updater.append("Contents"); + updater.append("MacOS"); + updater.append("org.mozilla.updater"); + } + return updater; +} + +/** + * Logs the contents of an update log and for maintenance service tests this + * will log the contents of the latest maintenanceservice.log. + * + * @param aLogLeafName + * The leaf name of the update log. + */ +function logUpdateLog(aLogLeafName) { + let updateLog = getUpdateLog(aLogLeafName); + if (updateLog.exists()) { + // xpcshell tests won't display the entire contents so log each line. + let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n"); + updateLogContents = replaceLogPaths(updateLogContents); + let aryLogContents = updateLogContents.split("\n"); + logTestInfo("contents of " + updateLog.path + ":"); + aryLogContents.forEach(function RU_LC_FE(aLine) { + logTestInfo(aLine); + }); + } else { + logTestInfo("update log doesn't exist, path: " + updateLog.path); + } + + if (IS_SERVICE_TEST) { + let serviceLog = getMaintSvcDir(); + serviceLog.append("logs"); + serviceLog.append("maintenanceservice.log"); + if (serviceLog.exists()) { + // xpcshell tests won't display the entire contents so log each line. + let serviceLogContents = readFileBytes(serviceLog).replace(/\r\n/g, "\n"); + serviceLogContents = replaceLogPaths(serviceLogContents); + let aryLogContents = serviceLogContents.split("\n"); + logTestInfo("contents of " + serviceLog.path + ":"); + aryLogContents.forEach(function RU_LC_FE(aLine) { + logTestInfo(aLine); + }); + } else { + logTestInfo("maintenance service log doesn't exist, path: " + + serviceLog.path); + } + } +} + +/** + * Gets the maintenance service log contents. + */ +function readServiceLogFile() { + let file = getMaintSvcDir(); + file.append("logs"); + file.append("maintenanceservice.log"); + return readFile(file); +} + +/** + * Launches the updater binary to apply an update for updater tests. + * + * @param aExpectedStatus + * The expected value of update.status when the test finishes. For + * service tests passing STATE_PENDING or STATE_APPLIED will change the + * value to STATE_PENDING_SVC and STATE_APPLIED_SVC respectively. + * @param aSwitchApp + * If true the update should switch the application with an updated + * staged application and if false the update should be applied to the + * installed application. + * @param aExpectedExitValue + * The expected exit value from the updater binary for non-service + * tests. + * @param aCheckSvcLog + * Whether the service log should be checked for service tests. + * @param aPatchDirPath (optional) + * When specified the patch directory path to use for invalid argument + * tests otherwise the normal path will be used. + * @param aInstallDirPath (optional) + * When specified the install directory path to use for invalid + * argument tests otherwise the normal path will be used. + * @param aApplyToDirPath (optional) + * When specified the apply to / working directory path to use for + * invalid argument tests otherwise the normal path will be used. + * @param aCallbackPath (optional) + * When specified the callback path to use for invalid argument tests + * otherwise the normal path will be used. + */ +function runUpdate(aExpectedStatus, aSwitchApp, aExpectedExitValue, aCheckSvcLog, + aPatchDirPath, aInstallDirPath, aApplyToDirPath, + aCallbackPath) { + let isInvalidArgTest = !!aPatchDirPath || !!aInstallDirPath || + !!aApplyToDirPath || aCallbackPath; + + let svcOriginalLog; + if (IS_SERVICE_TEST) { + copyFileToTestAppDir(FILE_MAINTENANCE_SERVICE_BIN, false); + copyFileToTestAppDir(FILE_MAINTENANCE_SERVICE_INSTALLER_BIN, false); + if (aCheckSvcLog) { + svcOriginalLog = readServiceLogFile(); + } + } + + // Copy the updater binary to the directory where it will apply updates. + let updateBin = copyTestUpdaterForRunUsingUpdater(); + Assert.ok(updateBin.exists(), + MSG_SHOULD_EXIST + getMsgPath(updateBin.path)); + + let updatesDirPath = aPatchDirPath || getUpdatesPatchDir().path; + let installDirPath = aInstallDirPath || getApplyDirFile(null, true).path; + let applyToDirPath = aApplyToDirPath || getApplyDirFile(null, true).path; + let stageDirPath = aApplyToDirPath || getStageDirFile(null, true).path; + + let callbackApp = getApplyDirFile(DIR_RESOURCES + gCallbackBinFile); + callbackApp.permissions = PERMS_DIRECTORY; + + setAppBundleModTime(); + + let args = [updatesDirPath, installDirPath]; + if (aSwitchApp) { + args[2] = stageDirPath; + args[3] = "0/replace"; + } else { + args[2] = applyToDirPath; + args[3] = "0"; + } + + let launchBin = IS_SERVICE_TEST && isInvalidArgTest ? callbackApp : updateBin; + + if (!isInvalidArgTest) { + args = args.concat([callbackApp.parent.path, callbackApp.path]); + args = args.concat(gCallbackArgs); + } else if (IS_SERVICE_TEST) { + args = ["launch-service", updateBin.path].concat(args); + } else if (aCallbackPath) { + args = args.concat([callbackApp.parent.path, aCallbackPath]); + } + + debugDump("launching the program: " + launchBin.path + " " + args.join(" ")); + + if (aSwitchApp && !isInvalidArgTest) { + // We want to set the env vars again + gShouldResetEnv = undefined; + } + + setEnvironment(); + + let process = Cc["@mozilla.org/process/util;1"]. + createInstance(Ci.nsIProcess); + process.init(launchBin); + process.run(true, args, args.length); + + resetEnvironment(); + + let status = readStatusFile(); + if ((!IS_SERVICE_TEST && process.exitValue != aExpectedExitValue) || + status != aExpectedStatus) { + if (process.exitValue != aExpectedExitValue) { + logTestInfo("updater exited with unexpected value! Got: " + + process.exitValue + ", Expected: " + aExpectedExitValue); + } + if (status != aExpectedStatus) { + logTestInfo("update status is not the expected status! Got: " + status + + ", Expected: " + aExpectedStatus); + } + logUpdateLog(FILE_LAST_UPDATE_LOG); + } + + if (!IS_SERVICE_TEST) { + Assert.equal(process.exitValue, aExpectedExitValue, + "the process exit value" + MSG_SHOULD_EQUAL); + } + Assert.equal(status, aExpectedStatus, + "the update status" + MSG_SHOULD_EQUAL); + + if (IS_SERVICE_TEST && aCheckSvcLog) { + let contents = readServiceLogFile(); + Assert.notEqual(contents, svcOriginalLog, + "the contents of the maintenanceservice.log should not " + + "be the same as the original contents"); + if (!isInvalidArgTest) { + Assert.notEqual(contents.indexOf(LOG_SVC_SUCCESSFUL_LAUNCH), -1, + "the contents of the maintenanceservice.log should " + + "contain the successful launch string"); + } + } + + do_execute_soon(runUpdateFinished); +} + +/** + * Launches the helper binary synchronously with the specified arguments for + * updater tests. + * + * @param aArgs + * The arguments to pass to the helper binary. + * @return the process exit value returned by the helper binary. + */ +function runTestHelperSync(aArgs) { + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let process = Cc["@mozilla.org/process/util;1"]. + createInstance(Ci.nsIProcess); + process.init(helperBin); + debugDump("Running " + helperBin.path + " " + aArgs.join(" ")); + process.run(true, aArgs, aArgs.length); + return process.exitValue; +} + +/** + * Creates a symlink for updater tests. + */ +function createSymlink() { + let args = ["setup-symlink", "moz-foo", "moz-bar", "target", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link"]; + let exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, + "the helper process exit value should be 0"); + getApplyDirFile(DIR_RESOURCES + "link", false).permissions = 0o666; + args = ["setup-symlink", "moz-foo2", "moz-bar2", "target2", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link2", "change-perm"]; + exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, + "the helper process exit value should be 0"); +} + +/** + * Removes a symlink for updater tests. + */ +function removeSymlink() { + let args = ["remove-symlink", "moz-foo", "moz-bar", "target", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link"]; + let exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, + "the helper process exit value should be 0"); + args = ["remove-symlink", "moz-foo2", "moz-bar2", "target2", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link2"]; + exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, + "the helper process exit value should be 0"); +} + +/** + * Checks a symlink for updater tests. + */ +function checkSymlink() { + let args = ["check-symlink", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link"]; + let exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, + "the helper process exit value should be 0"); +} + +/** + * Sets the active update and related information for updater tests. + */ +function setupActiveUpdate() { + let state = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + let channel = gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL); + let patches = getLocalPatchString(null, null, null, null, null, "true", + state); + let updates = getLocalUpdateString(patches, null, null, null, null, null, + null, null, null, null, "true", channel); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeVersionFile(DEFAULT_UPDATE_VERSION); + writeStatusFile(state); + reloadUpdateManagerData(); + Assert.ok(!!gUpdateManager.activeUpdate, + "the active update should be defined"); +} + +/** + * Gets the specified update log. + * + * @param aLogLeafName + * The leaf name of the log to get. + * @return nsIFile for the update log. + */ +function getUpdateLog(aLogLeafName) { + let updateLog = getUpdatesDir(); + if (aLogLeafName == FILE_UPDATE_LOG) { + updateLog.append(DIR_PATCH); + } + updateLog.append(aLogLeafName); + return updateLog; +} + +/** + * The update-staged observer for the call to nsIUpdateProcessor:processUpdate. + */ +const gUpdateStagedObserver = { + observe: function(aSubject, aTopic, aData) { + debugDump("observe called with topic: " + aTopic + ", data: " + aData); + if (aTopic == "update-staged") { + Services.obs.removeObserver(gUpdateStagedObserver, "update-staged"); + // The environment is reset after the update-staged observer topic because + // processUpdate in nsIUpdateProcessor uses a new thread and clearing the + // environment immediately after calling processUpdate can clear the + // environment before the updater is launched. + resetEnvironment(); + // Use do_execute_soon to prevent any failures from propagating to the + // update service. + do_execute_soon(checkUpdateStagedState.bind(null, aData)); + } + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) +}; + +/** + * Stages an update using nsIUpdateProcessor:processUpdate for updater tests. + * + * @param aCheckSvcLog + * Whether the service log should be checked for service tests. + */ +function stageUpdate(aCheckSvcLog) { + debugDump("start - attempting to stage update"); + + if (IS_SERVICE_TEST && aCheckSvcLog) { + gSvcOriginalLogContents = readServiceLogFile(); + } + + Services.obs.addObserver(gUpdateStagedObserver, "update-staged", false); + + setAppBundleModTime(); + setEnvironment(); + // Stage the update. + Cc["@mozilla.org/updates/update-processor;1"]. + createInstance(Ci.nsIUpdateProcessor). + processUpdate(gUpdateManager.activeUpdate); + + // The environment is not reset here because processUpdate in + // nsIUpdateProcessor uses a new thread and clearing the environment + // immediately after calling processUpdate can clear the environment before + // the updater is launched. Instead it is reset after the update-staged + // observer topic. + + debugDump("finish - attempting to stage update"); +} + +/** + * Checks that the update state is correct as well as the expected files are + * present after staging and update for updater tests and then calls + * stageUpdateFinished. + * + * @param aUpdateState + * The update state received by the observer notification. + */ +function checkUpdateStagedState(aUpdateState) { + if (IS_WIN) { + if (IS_SERVICE_TEST) { + waitForServiceStop(false); + } else { + let updater = getApplyDirFile(FILE_UPDATER_BIN, true); + if (isFileInUse(updater)) { + do_timeout(FILE_IN_USE_TIMEOUT_MS, + checkUpdateStagedState.bind(null, aUpdateState)); + return; + } + } + } + + Assert.equal(aUpdateState, STATE_AFTER_STAGE, + "the notified state" + MSG_SHOULD_EQUAL); + + if (!gStagingRemovedUpdate) { + Assert.equal(readStatusState(), STATE_AFTER_STAGE, + "the status file state" + MSG_SHOULD_EQUAL); + + Assert.equal(gUpdateManager.activeUpdate.state, STATE_AFTER_STAGE, + "the update state" + MSG_SHOULD_EQUAL); + } + + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_STAGE, + "the update state" + MSG_SHOULD_EQUAL); + + let log = getUpdateLog(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + let stageDir = getStageDirFile(null, true); + if (STATE_AFTER_STAGE == STATE_APPLIED || + STATE_AFTER_STAGE == STATE_APPLIED_SVC) { + Assert.ok(stageDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(stageDir.path)); + } else { + Assert.ok(!stageDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(stageDir.path)); + } + + if (IS_SERVICE_TEST && gSvcOriginalLogContents !== undefined) { + let contents = readServiceLogFile(); + Assert.notEqual(contents, gSvcOriginalLogContents, + "the contents of the maintenanceservice.log should not " + + "be the same as the original contents"); + Assert.notEqual(contents.indexOf(LOG_SVC_SUCCESSFUL_LAUNCH), -1, + "the contents of the maintenanceservice.log should " + + "contain the successful launch string"); + } + + do_execute_soon(stageUpdateFinished); +} + +/** + * Helper function to check whether the maintenance service updater tests should + * run. See bug 711660 for more details. + * + * @return true if the test should run and false if it shouldn't. + */ +function shouldRunServiceTest() { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + let binDir = getGREBinDir(); + let updaterBin = binDir.clone(); + updaterBin.append(FILE_UPDATER_BIN); + Assert.ok(updaterBin.exists(), + MSG_SHOULD_EXIST + ", leafName: " + updaterBin.leafName); + + let updaterBinPath = updaterBin.path; + if (/ /.test(updaterBinPath)) { + updaterBinPath = '"' + updaterBinPath + '"'; + } + + let isBinSigned = isBinarySigned(updaterBinPath); + + const REG_PATH = "SOFTWARE\\Mozilla\\MaintenanceService\\" + + "3932ecacee736d366d6436db0f55bce4"; + let key = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + try { + key.open(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, REG_PATH, + Ci.nsIWindowsRegKey.ACCESS_READ | key.WOW64_64); + } catch (e) { + // The build system could sign the files and not have the test registry key + // in which case we should fail the test if the updater binary is signed so + // the build system can be fixed by adding the registry key. + if (IS_AUTHENTICODE_CHECK_ENABLED) { + Assert.ok(!isBinSigned, + "the updater.exe binary should not be signed when the test " + + "registry key doesn't exist (if it is, build system " + + "configuration bug?)"); + } + + logTestInfo("this test can only run on the buildbot build system at this " + + "time"); + return false; + } + + // Check to make sure the service is installed + let args = ["wait-for-service-stop", "MozillaMaintenance", "10"]; + let exitValue = runTestHelperSync(args); + Assert.notEqual(exitValue, 0xEE, "the maintenance service should be " + + "installed (if not, build system configuration bug?)"); + + if (IS_AUTHENTICODE_CHECK_ENABLED) { + // The test registry key exists and IS_AUTHENTICODE_CHECK_ENABLED is true + // so the binaries should be signed. To run the test locally + // DISABLE_UPDATER_AUTHENTICODE_CHECK can be defined. + Assert.ok(isBinSigned, + "the updater.exe binary should be signed (if not, build system " + + "configuration bug?)"); + } + + // In case the machine is running an old maintenance service or if it + // is not installed, and permissions exist to install it. Then install + // the newer bin that we have since all of the other checks passed. + return attemptServiceInstall(); +} + +/** + * Helper function to check whether the a binary is signed. + * + * @param aBinPath + * The path to the file to check if it is signed. + * @return true if the file is signed and false if it isn't. + */ +function isBinarySigned(aBinPath) { + let args = ["check-signature", aBinPath]; + let exitValue = runTestHelperSync(args); + if (exitValue != 0) { + logTestInfo("binary is not signed. " + FILE_HELPER_BIN + " returned " + + exitValue + " for file " + aBinPath); + return false; + } + return true; +} + +/** + * Helper function for asynchronously setting up the application files required + * to launch the application for the updater tests by either copying or creating + * symlinks for the files. This is needed for Windows debug builds which can + * lock a file that is being copied so that the tests can run in parallel. After + * the files have been copied the setupUpdaterTestFinished function will be + * called. + */ +function setupAppFilesAsync() { + gTimeoutRuns++; + try { + setupAppFiles(); + } catch (e) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while trying to setup application " + + "files! Exception: " + e); + } + do_execute_soon(setupAppFilesAsync); + return; + } + + do_execute_soon(setupUpdaterTestFinished); +} + +/** + * Helper function for setting up the application files required to launch the + * application for the updater tests by either copying or creating symlinks to + * the files. + */ +function setupAppFiles() { + debugDump("start - copying or creating symlinks to application files " + + "for the test"); + + let destDir = getApplyDirFile(null, true); + if (!destDir.exists()) { + try { + destDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } catch (e) { + logTestInfo("unable to create directory! Path: " + destDir.path + + ", Exception: " + e); + do_throw(e); + } + } + + // Required files for the application or the test that aren't listed in the + // dependentlibs.list file. + let appFiles = [{relPath: FILE_APP_BIN, + inGreDir: false}, + {relPath: FILE_APPLICATION_INI, + inGreDir: true}, + {relPath: "dependentlibs.list", + inGreDir: true}]; + + // On Linux the updater.png must also be copied + if (IS_UNIX && !IS_MACOSX) { + appFiles.push({relPath: "icons/updater.png", + inGreDir: true}); + } + + // Read the dependent libs file leafnames from the dependentlibs.list file + // into the array. + let deplibsFile = gGREDirOrig.clone(); + deplibsFile.append("dependentlibs.list"); + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fis.init(deplibsFile, 0x01, 0o444, Ci.nsIFileInputStream.CLOSE_ON_EOF); + fis.QueryInterface(Ci.nsILineInputStream); + + let hasMore; + let line = {}; + do { + hasMore = fis.readLine(line); + appFiles.push({relPath: line.value, + inGreDir: false}); + } while (hasMore); + + fis.close(); + + appFiles.forEach(function CMAF_FLN_FE(aAppFile) { + copyFileToTestAppDir(aAppFile.relPath, aAppFile.inGreDir); + }); + + copyTestUpdaterToBinDir(); + + debugDump("finish - copying or creating symlinks to application files " + + "for the test"); +} + +/** + * Copies the specified files from the dist/bin directory into the test's + * application directory. + * + * @param aFileRelPath + * The relative path to the source and the destination of the file to + * copy. + * @param aInGreDir + * Whether the file is located in the GRE directory which is + * <bundle>/Contents/Resources on Mac OS X and is the installation + * directory on all other platforms. If false the file must be in the + * GRE Binary directory which is <bundle>/Contents/MacOS on Mac OS X + * and is the installation directory on on all other platforms. + */ +function copyFileToTestAppDir(aFileRelPath, aInGreDir) { + // gGREDirOrig and gGREBinDirOrig must always be cloned when changing its + // properties + let srcFile = aInGreDir ? gGREDirOrig.clone() : gGREBinDirOrig.clone(); + let destFile = aInGreDir ? getGREDir() : getGREBinDir(); + let fileRelPath = aFileRelPath; + let pathParts = fileRelPath.split("/"); + for (let i = 0; i < pathParts.length; i++) { + if (pathParts[i]) { + srcFile.append(pathParts[i]); + destFile.append(pathParts[i]); + } + } + + if (IS_MACOSX && !srcFile.exists()) { + debugDump("unable to copy file since it doesn't exist! Checking if " + + fileRelPath + ".app exists. Path: " + srcFile.path); + // gGREDirOrig and gGREBinDirOrig must always be cloned when changing its + // properties + srcFile = aInGreDir ? gGREDirOrig.clone() : gGREBinDirOrig.clone(); + destFile = aInGreDir ? getGREDir() : getGREBinDir(); + for (let i = 0; i < pathParts.length; i++) { + if (pathParts[i]) { + srcFile.append(pathParts[i] + (pathParts.length - 1 == i ? ".app" : "")); + destFile.append(pathParts[i] + (pathParts.length - 1 == i ? ".app" : "")); + } + } + fileRelPath = fileRelPath + ".app"; + } + Assert.ok(srcFile.exists(), + MSG_SHOULD_EXIST + ", leafName: " + srcFile.leafName); + + // Symlink libraries. Note that the XUL library on Mac OS X doesn't have a + // file extension and shouldSymlink will always be false on Windows. + let shouldSymlink = (pathParts[pathParts.length - 1] == "XUL" || + fileRelPath.substr(fileRelPath.length - 3) == ".so" || + fileRelPath.substr(fileRelPath.length - 6) == ".dylib"); + if (!shouldSymlink) { + if (!destFile.exists()) { + try { + srcFile.copyToFollowingLinks(destFile.parent, destFile.leafName); + } catch (e) { + // Just in case it is partially copied + if (destFile.exists()) { + try { + destFile.remove(true); + } catch (ex) { + logTestInfo("unable to remove file that failed to copy! Path: " + + destFile.path); + } + } + do_throw("Unable to copy file! Path: " + srcFile.path + + ", Exception: " + ex); + } + } + } else { + try { + if (destFile.exists()) { + destFile.remove(false); + } + let ln = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + ln.initWithPath("/bin/ln"); + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(ln); + let args = ["-s", srcFile.path, destFile.path]; + process.run(true, args, args.length); + Assert.ok(destFile.isSymlink(), + destFile.leafName + " should be a symlink"); + } catch (e) { + do_throw("Unable to create symlink for file! Path: " + srcFile.path + + ", Exception: " + e); + } + } +} + +/** + * Attempts to upgrade the maintenance service if permissions are allowed. + * This is useful for XP where we have permission to upgrade in case an + * older service installer exists. Also if the user manually installed into + * a unprivileged location. + * + * @return true if the installed service is from this build. If the installed + * service is not from this build the test will fail instead of + * returning false. + */ +function attemptServiceInstall() { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + let maintSvcDir = getMaintSvcDir(); + Assert.ok(maintSvcDir.exists(), + MSG_SHOULD_EXIST + ", leafName: " + maintSvcDir.leafName); + let oldMaintSvcBin = maintSvcDir.clone(); + oldMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN); + Assert.ok(oldMaintSvcBin.exists(), + MSG_SHOULD_EXIST + ", leafName: " + oldMaintSvcBin.leafName); + let buildMaintSvcBin = getGREBinDir(); + buildMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN); + if (readFileBytes(oldMaintSvcBin) == readFileBytes(buildMaintSvcBin)) { + debugDump("installed maintenance service binary is the same as the " + + "build's maintenance service binary"); + return true; + } + let backupMaintSvcBin = maintSvcDir.clone(); + backupMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN + ".backup"); + try { + if (backupMaintSvcBin.exists()) { + backupMaintSvcBin.remove(false); + } + oldMaintSvcBin.moveTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN + ".backup"); + buildMaintSvcBin.copyTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN); + backupMaintSvcBin.remove(false); + } catch (e) { + // Restore the original file in case the moveTo was successful. + if (backupMaintSvcBin.exists()) { + oldMaintSvcBin = maintSvcDir.clone(); + oldMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN); + if (!oldMaintSvcBin.exists()) { + backupMaintSvcBin.moveTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN); + } + } + Assert.ok(false, "should be able copy the test maintenance service to " + + "the maintenance service directory (if not, build system " + + "configuration bug?), path: " + maintSvcDir.path); + } + + return true; +} + +/** + * Waits for the applications that are launched by the maintenance service to + * stop. + */ +function waitServiceApps() { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + // maintenanceservice_installer.exe is started async during updates. + waitForApplicationStop("maintenanceservice_installer.exe"); + // maintenanceservice_tmp.exe is started async from the service installer. + waitForApplicationStop("maintenanceservice_tmp.exe"); + // In case the SCM thinks the service is stopped, but process still exists. + waitForApplicationStop("maintenanceservice.exe"); +} + +/** + * Waits for the maintenance service to stop. + */ +function waitForServiceStop(aFailTest) { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + waitServiceApps(); + debugDump("waiting for the maintenance service to stop if necessary"); + // Use the helper bin to ensure the service is stopped. If not stopped, then + // wait for the service to stop (at most 120 seconds). + let args = ["wait-for-service-stop", "MozillaMaintenance", "120"]; + let exitValue = runTestHelperSync(args); + Assert.notEqual(exitValue, 0xEE, + "the maintenance service should exist"); + if (exitValue != 0) { + if (aFailTest) { + Assert.ok(false, "the maintenance service should stop, process exit " + + "value: " + exitValue); + } + logTestInfo("maintenance service did not stop which may cause test " + + "failures later, process exit value: " + exitValue); + } else { + debugDump("service stopped"); + } + waitServiceApps(); +} + +/** + * Waits for the specified application to stop. + * + * @param aApplication + * The application binary name to wait until it has stopped. + */ +function waitForApplicationStop(aApplication) { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + debugDump("waiting for " + aApplication + " to stop if necessary"); + // Use the helper bin to ensure the application is stopped. If not stopped, + // then wait for it to stop (at most 120 seconds). + let args = ["wait-for-application-exit", aApplication, "120"]; + let exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, + "the process should have stopped, process name: " + + aApplication); +} + + +/** + * Gets the platform specific shell binary that is launched using nsIProcess and + * in turn launches a binary used for the test (e.g. application, updater, + * etc.). A shell is used so debug console output can be redirected to a file so + * it doesn't end up in the test log. + * + * @return nsIFile for the shell binary to launch using nsIProcess. + */ +function getLaunchBin() { + let launchBin; + if (IS_WIN) { + launchBin = Services.dirsvc.get("WinD", Ci.nsIFile); + launchBin.append("System32"); + launchBin.append("cmd.exe"); + } else { + launchBin = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsILocalFile); + launchBin.initWithPath("/bin/sh"); + } + Assert.ok(launchBin.exists(), + MSG_SHOULD_EXIST + getMsgPath(launchBin.path)); + + return launchBin; +} + + +/** + * Locks a Windows directory. + * + * @param aDirPath + * The test file object that describes the file to make in use. + */ +function lockDirectory(aDirPath) { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + debugDump("start - locking installation directory"); + const LPCWSTR = ctypes.char16_t.ptr; + const DWORD = ctypes.uint32_t; + const LPVOID = ctypes.voidptr_t; + const GENERIC_READ = 0x80000000; + const FILE_SHARE_READ = 1; + const FILE_SHARE_WRITE = 2; + const OPEN_EXISTING = 3; + const FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + const INVALID_HANDLE_VALUE = LPVOID(0xffffffff); + let kernel32 = ctypes.open("kernel32"); + let CreateFile = kernel32.declare("CreateFileW", ctypes.default_abi, + LPVOID, LPCWSTR, DWORD, DWORD, + LPVOID, DWORD, DWORD, LPVOID); + gHandle = CreateFile(aDirPath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, LPVOID(0), + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, LPVOID(0)); + Assert.notEqual(gHandle.toString(), INVALID_HANDLE_VALUE.toString(), + "the handle should not equal INVALID_HANDLE_VALUE"); + kernel32.close(); + debugDump("finish - locking installation directory"); +} + +/** + * Launches the test helper binary to make it in use for updater tests and then + * calls waitForHelperSleep. + * + * @param aTestFile + * The test file object that describes the file to make in use. + */ +function runHelperFileInUse(aRelPath, aCopyTestHelper) { + logTestInfo("aRelPath: " + aRelPath); + // Launch an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let fileInUseBin = getApplyDirFile(aRelPath); + if (aCopyTestHelper) { + fileInUseBin.remove(false); + helperBin.copyTo(fileInUseBin.parent, fileInUseBin.leafName); + } + fileInUseBin.permissions = PERMS_DIRECTORY; + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = Cc["@mozilla.org/process/util;1"]. + createInstance(Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + do_execute_soon(waitForHelperSleep); +} + +/** + * Launches the test helper binary and locks a file specified on the command + * line for updater tests and then calls waitForHelperSleep. + * + * @param aTestFile + * The test file object that describes the file to lock. + */ +function runHelperLockFile(aTestFile) { + // Exclusively lock an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let helperDestDir = getApplyDirFile(DIR_RESOURCES); + helperBin.copyTo(helperDestDir, FILE_HELPER_BIN); + helperBin = getApplyDirFile(DIR_RESOURCES + FILE_HELPER_BIN); + // Strip off the first two directories so the path has to be from the helper's + // working directory. + let lockFileRelPath = aTestFile.relPathDir.split("/"); + if (IS_MACOSX) { + lockFileRelPath = lockFileRelPath.slice(2); + } + lockFileRelPath = lockFileRelPath.join("/") + "/" + aTestFile.fileName; + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT, lockFileRelPath]; + let helperProcess = Cc["@mozilla.org/process/util;1"]. + createInstance(Ci.nsIProcess); + helperProcess.init(helperBin); + helperProcess.run(false, args, args.length); + + do_execute_soon(waitForHelperSleep); +} + +/** + * Helper function that waits until the helper has completed its operations and + * calls waitForHelperSleepFinished when it is finished. + */ +function waitForHelperSleep() { + gTimeoutRuns++; + // Give the lock file process time to lock the file before updating otherwise + // this test can fail intermittently on Windows debug builds. + let output = getApplyDirFile(DIR_RESOURCES + "output", true); + if (readFile(output) != "sleeping\n") { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the helper to " + + "finish its operation. Path: " + output.path); + } + // Uses do_timeout instead of do_execute_soon to lessen log spew. + do_timeout(FILE_IN_USE_TIMEOUT_MS, waitForHelperSleep); + return; + } + try { + output.remove(false); + } catch (e) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the helper " + + "message file to no longer be in use. Path: " + output.path); + } + debugDump("failed to remove file. Path: " + output.path); + // Uses do_timeout instead of do_execute_soon to lessen log spew. + do_timeout(FILE_IN_USE_TIMEOUT_MS, waitForHelperSleep); + return; + } + waitForHelperSleepFinished(); +} + +/** + * Helper function that waits until the helper has finished its operations + * before calling waitForHelperFinishFileUnlock to verify that the helper's + * input and output directories are no longer in use. + */ +function waitForHelperFinished() { + // Give the lock file process time to lock the file before updating otherwise + // this test can fail intermittently on Windows debug builds. + let output = getApplyDirFile(DIR_RESOURCES + "output", true); + if (readFile(output) != "finished\n") { + // Uses do_timeout instead of do_execute_soon to lessen log spew. + do_timeout(FILE_IN_USE_TIMEOUT_MS, waitForHelperFinished); + return; + } + // Give the lock file process time to unlock the file before deleting the + // input and output files. + waitForHelperFinishFileUnlock(); +} + +/** + * Helper function that waits until the helper's input and output files are no + * longer in use before calling waitForHelperExitFinished. + */ +function waitForHelperFinishFileUnlock() { + try { + let output = getApplyDirFile(DIR_RESOURCES + "output", true); + if (output.exists()) { + output.remove(false); + } + let input = getApplyDirFile(DIR_RESOURCES + "input", true); + if (input.exists()) { + input.remove(false); + } + } catch (e) { + // Give the lock file process time to unlock the file before deleting the + // input and output files. + do_execute_soon(waitForHelperFinishFileUnlock); + return; + } + do_execute_soon(waitForHelperExitFinished); +} + +/** + * Helper function to tell the helper to finish and exit its sleep state. + */ +function waitForHelperExit() { + let input = getApplyDirFile(DIR_RESOURCES + "input", true); + writeFile(input, "finish\n"); + waitForHelperFinished(); +} + +/** + * Helper function for updater binary tests that creates the files and + * directories used by the test. + * + * @param aMarFile + * The mar file for the update test. + * @param aPostUpdateAsync + * When null the updater.ini is not created otherwise this parameter + * is passed to createUpdaterINI. + * @param aPostUpdateExeRelPathPrefix + * When aPostUpdateAsync null this value is ignored otherwise it is + * passed to createUpdaterINI. + */ +function setupUpdaterTest(aMarFile, aPostUpdateAsync, + aPostUpdateExeRelPathPrefix = "") { + let updatesPatchDir = getUpdatesPatchDir(); + if (!updatesPatchDir.exists()) { + updatesPatchDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } + // Copy the mar that will be applied + let mar = getTestDirFile(aMarFile); + mar.copyToFollowingLinks(updatesPatchDir, FILE_UPDATE_MAR); + + let helperBin = getTestDirFile(FILE_HELPER_BIN); + helperBin.permissions = PERMS_DIRECTORY; + let afterApplyBinDir = getApplyDirFile(DIR_RESOURCES, true); + helperBin.copyToFollowingLinks(afterApplyBinDir, gCallbackBinFile); + helperBin.copyToFollowingLinks(afterApplyBinDir, gPostUpdateBinFile); + + gTestFiles.forEach(function SUT_TF_FE(aTestFile) { + if (aTestFile.originalFile || aTestFile.originalContents) { + let testDir = getApplyDirFile(aTestFile.relPathDir, true); + if (!testDir.exists()) { + testDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } + + let testFile; + if (aTestFile.originalFile) { + testFile = getTestDirFile(aTestFile.originalFile); + testFile.copyToFollowingLinks(testDir, aTestFile.fileName); + testFile = getApplyDirFile(aTestFile.relPathDir + aTestFile.fileName); + } else { + testFile = getApplyDirFile(aTestFile.relPathDir + aTestFile.fileName, + true); + writeFile(testFile, aTestFile.originalContents); + } + + // Skip these tests on Windows since chmod doesn't really set permissions + // on Windows. + if (!IS_WIN && aTestFile.originalPerms) { + testFile.permissions = aTestFile.originalPerms; + // Store the actual permissions on the file for reference later after + // setting the permissions. + if (!aTestFile.comparePerms) { + aTestFile.comparePerms = testFile.permissions; + } + } + } + }); + + // Add the test directory that will be updated for a successful update or left + // in the initial state for a failed update. + gTestDirs.forEach(function SUT_TD_FE(aTestDir) { + let testDir = getApplyDirFile(aTestDir.relPathDir, true); + if (!testDir.exists()) { + testDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } + + if (aTestDir.files) { + aTestDir.files.forEach(function SUT_TD_F_FE(aTestFile) { + let testFile = getApplyDirFile(aTestDir.relPathDir + aTestFile, true); + if (!testFile.exists()) { + testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + } + }); + } + + if (aTestDir.subDirs) { + aTestDir.subDirs.forEach(function SUT_TD_SD_FE(aSubDir) { + let testSubDir = getApplyDirFile(aTestDir.relPathDir + aSubDir, true); + if (!testSubDir.exists()) { + testSubDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } + + if (aTestDir.subDirFiles) { + aTestDir.subDirFiles.forEach(function SUT_TD_SDF_FE(aTestFile) { + let testFile = getApplyDirFile(aTestDir.relPathDir + aSubDir + aTestFile, true); + if (!testFile.exists()) { + testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + } + }); + } + }); + } + }); + + setupActiveUpdate(); + + if (aPostUpdateAsync !== null) { + createUpdaterINI(aPostUpdateAsync, aPostUpdateExeRelPathPrefix); + } + + setupAppFilesAsync(); +} + +/** + * Helper function for updater binary tests that creates the update-settings.ini + * file. + */ +function createUpdateSettingsINI() { + let ini = getApplyDirFile(DIR_RESOURCES + FILE_UPDATE_SETTINGS_INI, true); + writeFile(ini, UPDATE_SETTINGS_CONTENTS); +} + +/** + * Helper function for updater binary tests that creates the updater.ini + * file. + * + * @param aIsExeAsync + * True or undefined if the post update process should be async. If + * undefined ExeAsync will not be added to the updater.ini file in + * order to test the default launch behavior which is async. + * @param aExeRelPathPrefix + * A string to prefix the ExeRelPath values in the updater.ini. + */ +function createUpdaterINI(aIsExeAsync, aExeRelPathPrefix) { + let exeArg = "ExeArg=post-update-async\n"; + let exeAsync = ""; + if (aIsExeAsync !== undefined) { + if (aIsExeAsync) { + exeAsync = "ExeAsync=true\n"; + } else { + exeArg = "ExeArg=post-update-sync\n"; + exeAsync = "ExeAsync=false\n"; + } + } + + if (aExeRelPathPrefix && IS_WIN) { + aExeRelPathPrefix = aExeRelPathPrefix.replace("/", "\\"); + } + + let exeRelPathMac = "ExeRelPath=" + aExeRelPathPrefix + DIR_RESOURCES + + gPostUpdateBinFile + "\n"; + let exeRelPathWin = "ExeRelPath=" + aExeRelPathPrefix + gPostUpdateBinFile + "\n"; + let updaterIniContents = "[Strings]\n" + + "Title=Update Test\n" + + "Info=Running update test " + gTestID + "\n\n" + + "[PostUpdateMac]\n" + + exeRelPathMac + + exeArg + + exeAsync + + "\n" + + "[PostUpdateWin]\n" + + exeRelPathWin + + exeArg + + exeAsync; + let updaterIni = getApplyDirFile(DIR_RESOURCES + FILE_UPDATER_INI, true); + writeFile(updaterIni, updaterIniContents); +} + +/** + * Gets the message log path used for assert checks to lessen the length printed + * to the log file. + * + * @param aPath + * The path to shorten for the log file. + * @return the message including the shortened path for the log file. + */ +function getMsgPath(aPath) { + return ", path: " + replaceLogPaths(aPath); +} + +/** + * Helper function that replaces the common part of paths in the update log's + * contents with <test_dir_path> for paths to the the test directory and + * <update_dir_path> for paths to the update directory. This is needed since + * Assert.equal will truncate what it prints to the xpcshell log file. + * + * @param aLogContents + * The update log file's contents. + * @return the log contents with the paths replaced. + */ +function replaceLogPaths(aLogContents) { + let logContents = aLogContents; + // Remove the majority of the path up to the test directory. This is needed + // since Assert.equal won't print long strings to the test logs. + let testDirPath = do_get_file(gTestID, false).path; + if (IS_WIN) { + // Replace \\ with \\\\ so the regexp works. + testDirPath = testDirPath.replace(/\\/g, "\\\\"); + } + logContents = logContents.replace(new RegExp(testDirPath, "g"), + "<test_dir_path>/" + gTestID); + let updatesDirPath = getMockUpdRootD().path; + if (IS_WIN) { + // Replace \\ with \\\\ so the regexp works. + updatesDirPath = updatesDirPath.replace(/\\/g, "\\\\"); + } + logContents = logContents.replace(new RegExp(updatesDirPath, "g"), + "<update_dir_path>/" + gTestID); + if (IS_WIN) { + // Replace \ with / + logContents = logContents.replace(/\\/g, "/"); + } + return logContents; +} + +/** + * Helper function for updater binary tests for verifying the contents of the + * update log after a successful update. + * + * @param aCompareLogFile + * The log file to compare the update log with. + * @param aStaged + * If the update log file is for a staged update. + * @param aReplace + * If the update log file is for a replace update. + * @param aExcludeDistDir + * Removes lines containing the distribution directory from the log + * file to compare the update log with. + */ +function checkUpdateLogContents(aCompareLogFile, aStaged = false, + aReplace = false, aExcludeDistDir = false) { + if (IS_UNIX && !IS_MACOSX) { + // The order that files are returned when enumerating the file system on + // Linux is not deterministic so skip checking the logs. + return; + } + + let updateLog = getUpdateLog(FILE_LAST_UPDATE_LOG); + let updateLogContents = readFileBytes(updateLog); + + // The channel-prefs.js is defined in gTestFilesCommon which will always be + // located to the end of gTestFiles when it is present. + if (gTestFiles.length > 1 && + gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" && + !gTestFiles[gTestFiles.length - 1].originalContents) { + updateLogContents = updateLogContents.replace(/.*defaults\/.*/g, ""); + } + + if (gTestFiles.length > 2 && + gTestFiles[gTestFiles.length - 2].fileName == FILE_UPDATE_SETTINGS_INI && + !gTestFiles[gTestFiles.length - 2].originalContents) { + updateLogContents = updateLogContents.replace(/.*update-settings.ini.*/g, ""); + } + + // Skip the source/destination lines since they contain absolute paths. + // These could be changed to relative paths using <test_dir_path> and + // <update_dir_path> + updateLogContents = updateLogContents.replace(/PATCH DIRECTORY.*/g, ""); + updateLogContents = updateLogContents.replace(/INSTALLATION DIRECTORY.*/g, ""); + updateLogContents = updateLogContents.replace(/WORKING DIRECTORY.*/g, ""); + // Skip lines that log failed attempts to open the callback executable. + updateLogContents = updateLogContents.replace(/NS_main: callback app file .*/g, ""); + + if (IS_MACOSX) { + // Skip lines that log moving the distribution directory for Mac v2 signing. + updateLogContents = updateLogContents.replace(/Moving old [^\n]*\nrename_file: .*/g, ""); + updateLogContents = updateLogContents.replace(/New distribution directory .*/g, ""); + } + + if (IS_WIN) { + // The FindFile results when enumerating the filesystem on Windows is not + // determistic so the results matching the following need to be fixed. + let re = new RegExp("([^\n]* 7\/7text1[^\n]*)\n" + + "([^\n]* 7\/7text0[^\n]*)\n", "g"); + updateLogContents = updateLogContents.replace(re, "$2\n$1\n"); + } + + if (aReplace) { + // Remove the lines which contain absolute paths + updateLogContents = updateLogContents.replace(/^Begin moving.*$/mg, ""); + updateLogContents = updateLogContents.replace(/^ensure_remove: failed to remove file: .*$/mg, ""); + updateLogContents = updateLogContents.replace(/^ensure_remove_recursive: unable to remove directory: .*$/mg, ""); + updateLogContents = updateLogContents.replace(/^Removing tmpDir failed, err: -1$/mg, ""); + updateLogContents = updateLogContents.replace(/^remove_recursive_on_reboot: .*$/mg, ""); + } + + // Remove carriage returns. + updateLogContents = updateLogContents.replace(/\r/g, ""); + // Replace error codes since they are different on each platform. + updateLogContents = updateLogContents.replace(/, err:.*\n/g, "\n"); + // Replace to make the log parsing happy. + updateLogContents = updateLogContents.replace(/non-fatal error /g, ""); + // Remove consecutive newlines + updateLogContents = updateLogContents.replace(/\n+/g, "\n"); + // Remove leading and trailing newlines + updateLogContents = updateLogContents.replace(/^\n|\n$/g, ""); + // Replace the log paths with <test_dir_path> and <update_dir_path> + updateLogContents = replaceLogPaths(updateLogContents); + + let compareLogContents = ""; + if (aCompareLogFile) { + compareLogContents = readFileBytes(getTestDirFile(aCompareLogFile)); + } + + if (aStaged) { + compareLogContents = PERFORMING_STAGED_UPDATE + "\n" + compareLogContents; + } + + // The channel-prefs.js is defined in gTestFilesCommon which will always be + // located to the end of gTestFiles. + if (gTestFiles.length > 1 && + gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" && + !gTestFiles[gTestFiles.length - 1].originalContents) { + compareLogContents = compareLogContents.replace(/.*defaults\/.*/g, ""); + } + + if (gTestFiles.length > 2 && + gTestFiles[gTestFiles.length - 2].fileName == FILE_UPDATE_SETTINGS_INI && + !gTestFiles[gTestFiles.length - 2].originalContents) { + compareLogContents = compareLogContents.replace(/.*update-settings.ini.*/g, ""); + } + + if (aExcludeDistDir) { + compareLogContents = compareLogContents.replace(/.*distribution\/.*/g, ""); + } + + // Remove leading and trailing newlines + compareLogContents = compareLogContents.replace(/\n+/g, "\n"); + // Remove leading and trailing newlines + compareLogContents = compareLogContents.replace(/^\n|\n$/g, ""); + + // Don't write the contents of the file to the log to reduce log spam + // unless there is a failure. + if (compareLogContents == updateLogContents) { + Assert.ok(true, "the update log contents" + MSG_SHOULD_EQUAL); + } else { + logTestInfo("the update log contents are not correct"); + logUpdateLog(FILE_LAST_UPDATE_LOG); + let aryLog = updateLogContents.split("\n"); + let aryCompare = compareLogContents.split("\n"); + // Pushing an empty string to both arrays makes it so either array's length + // can be used in the for loop below without going out of bounds. + aryLog.push(""); + aryCompare.push(""); + // xpcshell tests won't display the entire contents so log the first + // incorrect line. + for (let i = 0; i < aryLog.length; ++i) { + if (aryLog[i] != aryCompare[i]) { + logTestInfo("the first incorrect line in the update log is: " + + aryLog[i]); + Assert.equal(aryLog[i], aryCompare[i], + "the update log contents" + MSG_SHOULD_EQUAL); + } + } + // This should never happen! + do_throw("Unable to find incorrect update log contents!"); + } +} + +/** + * Helper function to check if the update log contains a string. + * + * @param aCheckString + * The string to check if the update log contains. + */ +function checkUpdateLogContains(aCheckString) { + let updateLog = getUpdateLog(FILE_LAST_UPDATE_LOG); + let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n"); + updateLogContents = replaceLogPaths(updateLogContents); + Assert.notEqual(updateLogContents.indexOf(aCheckString), -1, + "the update log contents should contain value: " + + aCheckString); +} + +/** + * Helper function for updater binary tests for verifying the state of files and + * directories after a successful update. + * + * @param aGetFileFunc + * The function used to get the files in the directory to be checked. + * @param aStageDirExists + * If true the staging directory will be tested for existence and if + * false the staging directory will be tested for non-existence. + * @param aToBeDeletedDirExists + * On Windows, if true the tobedeleted directory will be tested for + * existence and if false the tobedeleted directory will be tested for + * non-existence. On all othere platforms it will be tested for + * non-existence. + */ +function checkFilesAfterUpdateSuccess(aGetFileFunc, aStageDirExists = false, + aToBeDeletedDirExists = false) { + debugDump("testing contents of files after a successful update"); + gTestFiles.forEach(function CFAUS_TF_FE(aTestFile) { + let testFile = aGetFileFunc(aTestFile.relPathDir + aTestFile.fileName, true); + debugDump("testing file: " + testFile.path); + if (aTestFile.compareFile || aTestFile.compareContents) { + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + + // Skip these tests on Windows since chmod doesn't really set permissions + // on Windows. + if (!IS_WIN && aTestFile.comparePerms) { + // Check if the permssions as set in the complete mar file are correct. + Assert.equal(testFile.permissions & 0xfff, + aTestFile.comparePerms & 0xfff, + "the file permissions" + MSG_SHOULD_EQUAL); + } + + let fileContents1 = readFileBytes(testFile); + let fileContents2 = aTestFile.compareFile ? + readFileBytes(getTestDirFile(aTestFile.compareFile)) : + aTestFile.compareContents; + // Don't write the contents of the file to the log to reduce log spam + // unless there is a failure. + if (fileContents1 == fileContents2) { + Assert.ok(true, "the file contents" + MSG_SHOULD_EQUAL); + } else { + Assert.equal(fileContents1, fileContents2, + "the file contents" + MSG_SHOULD_EQUAL); + } + } else { + Assert.ok(!testFile.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path)); + } + }); + + debugDump("testing operations specified in removed-files were performed " + + "after a successful update"); + gTestDirs.forEach(function CFAUS_TD_FE(aTestDir) { + let testDir = aGetFileFunc(aTestDir.relPathDir, true); + debugDump("testing directory: " + testDir.path); + if (aTestDir.dirRemoved) { + Assert.ok(!testDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(testDir.path)); + } else { + Assert.ok(testDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(testDir.path)); + + if (aTestDir.files) { + aTestDir.files.forEach(function CFAUS_TD_F_FE(aTestFile) { + let testFile = aGetFileFunc(aTestDir.relPathDir + aTestFile, true); + if (aTestDir.filesRemoved) { + Assert.ok(!testFile.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path)); + } else { + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + } + }); + } + + if (aTestDir.subDirs) { + aTestDir.subDirs.forEach(function CFAUS_TD_SD_FE(aSubDir) { + let testSubDir = aGetFileFunc(aTestDir.relPathDir + aSubDir, true); + Assert.ok(testSubDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(testSubDir.path)); + if (aTestDir.subDirFiles) { + aTestDir.subDirFiles.forEach(function CFAUS_TD_SDF_FE(aTestFile) { + let testFile = aGetFileFunc(aTestDir.relPathDir + + aSubDir + aTestFile, true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + }); + } + }); + } + } + }); + + checkFilesAfterUpdateCommon(aGetFileFunc, aStageDirExists, + aToBeDeletedDirExists); +} + +/** + * Helper function for updater binary tests for verifying the state of files and + * directories after a failed update. + * + * @param aGetFileFunc + * The function used to get the files in the directory to be checked. + * @param aStageDirExists + * If true the staging directory will be tested for existence and if + * false the staging directory will be tested for non-existence. + * @param aToBeDeletedDirExists + * On Windows, if true the tobedeleted directory will be tested for + * existence and if false the tobedeleted directory will be tested for + * non-existence. On all othere platforms it will be tested for + * non-existence. + */ +function checkFilesAfterUpdateFailure(aGetFileFunc, aStageDirExists = false, + aToBeDeletedDirExists = false) { + debugDump("testing contents of files after a failed update"); + gTestFiles.forEach(function CFAUF_TF_FE(aTestFile) { + let testFile = aGetFileFunc(aTestFile.relPathDir + aTestFile.fileName, true); + debugDump("testing file: " + testFile.path); + if (aTestFile.compareFile || aTestFile.compareContents) { + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + + // Skip these tests on Windows since chmod doesn't really set permissions + // on Windows. + if (!IS_WIN && aTestFile.comparePerms) { + // Check the original permssions are retained on the file. + Assert.equal(testFile.permissions & 0xfff, + aTestFile.comparePerms & 0xfff, + "the file permissions" + MSG_SHOULD_EQUAL); + } + + let fileContents1 = readFileBytes(testFile); + let fileContents2 = aTestFile.compareFile ? + readFileBytes(getTestDirFile(aTestFile.compareFile)) : + aTestFile.compareContents; + // Don't write the contents of the file to the log to reduce log spam + // unless there is a failure. + if (fileContents1 == fileContents2) { + Assert.ok(true, "the file contents" + MSG_SHOULD_EQUAL); + } else { + Assert.equal(fileContents1, fileContents2, + "the file contents" + MSG_SHOULD_EQUAL); + } + } else { + Assert.ok(!testFile.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path)); + } + }); + + debugDump("testing operations specified in removed-files were not " + + "performed after a failed update"); + gTestDirs.forEach(function CFAUF_TD_FE(aTestDir) { + let testDir = aGetFileFunc(aTestDir.relPathDir, true); + Assert.ok(testDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(testDir.path)); + + if (aTestDir.files) { + aTestDir.files.forEach(function CFAUS_TD_F_FE(aTestFile) { + let testFile = aGetFileFunc(aTestDir.relPathDir + aTestFile, true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + }); + } + + if (aTestDir.subDirs) { + aTestDir.subDirs.forEach(function CFAUS_TD_SD_FE(aSubDir) { + let testSubDir = aGetFileFunc(aTestDir.relPathDir + aSubDir, true); + Assert.ok(testSubDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(testSubDir.path)); + if (aTestDir.subDirFiles) { + aTestDir.subDirFiles.forEach(function CFAUS_TD_SDF_FE(aTestFile) { + let testFile = aGetFileFunc(aTestDir.relPathDir + + aSubDir + aTestFile, true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + }); + } + }); + } + }); + + checkFilesAfterUpdateCommon(aGetFileFunc, aStageDirExists, + aToBeDeletedDirExists); +} + +/** + * Helper function for updater binary tests for verifying the state of common + * files and directories after a successful or failed update. + * + * @param aGetFileFunc + * the function used to get the files in the directory to be checked. + * @param aStageDirExists + * If true the staging directory will be tested for existence and if + * false the staging directory will be tested for non-existence. + * @param aToBeDeletedDirExists + * On Windows, if true the tobedeleted directory will be tested for + * existence and if false the tobedeleted directory will be tested for + * non-existence. On all othere platforms it will be tested for + * non-existence. + */ +function checkFilesAfterUpdateCommon(aGetFileFunc, aStageDirExists, + aToBeDeletedDirExists) { + debugDump("testing extra directories"); + let stageDir = getStageDirFile(null, true); + if (aStageDirExists) { + Assert.ok(stageDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(stageDir.path)); + } else { + Assert.ok(!stageDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(stageDir.path)); + } + + let toBeDeletedDirExists = IS_WIN ? aToBeDeletedDirExists : false; + let toBeDeletedDir = getApplyDirFile(DIR_TOBEDELETED, true); + if (toBeDeletedDirExists) { + Assert.ok(toBeDeletedDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(toBeDeletedDir.path)); + } else { + Assert.ok(!toBeDeletedDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(toBeDeletedDir.path)); + } + + let updatingDir = getApplyDirFile("updating", true); + Assert.ok(!updatingDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(updatingDir.path)); + + if (stageDir.exists()) { + updatingDir = stageDir.clone(); + updatingDir.append("updating"); + Assert.ok(!updatingDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(updatingDir.path)); + } + + debugDump("testing backup files should not be left behind in the " + + "application directory"); + let applyToDir = getApplyDirFile(null, true); + checkFilesInDirRecursive(applyToDir, checkForBackupFiles); + + if (stageDir.exists()) { + debugDump("testing backup files should not be left behind in the " + + "staging directory"); + applyToDir = getApplyDirFile(null, true); + checkFilesInDirRecursive(stageDir, checkForBackupFiles); + } +} + +/** + * Helper function for updater binary tests for verifying the contents of the + * updater callback application log which should contain the arguments passed to + * the callback application. + */ +function checkCallbackLog() { + let appLaunchLog = getApplyDirFile(DIR_RESOURCES + gCallbackArgs[1], true); + if (!appLaunchLog.exists()) { + // Uses do_timeout instead of do_execute_soon to lessen log spew. + do_timeout(FILE_IN_USE_TIMEOUT_MS, checkCallbackLog); + return; + } + + let expectedLogContents = gCallbackArgs.join("\n") + "\n"; + let logContents = readFile(appLaunchLog); + // It is possible for the log file contents check to occur before the log file + // contents are completely written so wait until the contents are the expected + // value. If the contents are never the expected value then the test will + // fail by timing out after gTimeoutRuns is greater than MAX_TIMEOUT_RUNS or + // the test harness times out the test. + if (logContents != expectedLogContents) { + gTimeoutRuns++; + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + logTestInfo("callback log contents are not correct"); + // This file doesn't contain full paths so there is no need to call + // replaceLogPaths. + let aryLog = logContents.split("\n"); + let aryCompare = expectedLogContents.split("\n"); + // Pushing an empty string to both arrays makes it so either array's length + // can be used in the for loop below without going out of bounds. + aryLog.push(""); + aryCompare.push(""); + // xpcshell tests won't display the entire contents so log the incorrect + // line. + for (let i = 0; i < aryLog.length; ++i) { + if (aryLog[i] != aryCompare[i]) { + logTestInfo("the first incorrect line in the callback log is: " + + aryLog[i]); + Assert.equal(aryLog[i], aryCompare[i], + "the callback log contents" + MSG_SHOULD_EQUAL); + } + } + // This should never happen! + do_throw("Unable to find incorrect callback log contents!"); + } + // Uses do_timeout instead of do_execute_soon to lessen log spew. + do_timeout(FILE_IN_USE_TIMEOUT_MS, checkCallbackLog); + return; + } + Assert.ok(true, "the callback log contents" + MSG_SHOULD_EQUAL); + + waitForFilesInUse(); +} + +/** + * Helper function for updater binary tests for getting the log and running + * files created by the test helper binary file when called with the post-update + * command line argument. + * + * @param aSuffix + * The string to append to the post update test helper binary path. + */ +function getPostUpdateFile(aSuffix) { + return getApplyDirFile(DIR_RESOURCES + gPostUpdateBinFile + aSuffix, true); +} + +/** + * Checks the contents of the updater post update binary log. When completed + * checkPostUpdateAppLogFinished will be called. + */ +function checkPostUpdateAppLog() { + // Only Mac OS X and Windows support post update. + if (IS_MACOSX || IS_WIN) { + gTimeoutRuns++; + let postUpdateLog = getPostUpdateFile(".log"); + if (!postUpdateLog.exists()) { + debugDump("postUpdateLog does not exist. Path: " + postUpdateLog.path); + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the post update " + + "process to create the post update log. Path: " + + postUpdateLog.path); + } + do_execute_soon(checkPostUpdateAppLog); + return; + } + + let logContents = readFile(postUpdateLog); + // It is possible for the log file contents check to occur before the log file + // contents are completely written so wait until the contents are the expected + // value. If the contents are never the expected value then the test will + // fail by timing out after gTimeoutRuns is greater than MAX_TIMEOUT_RUNS or + // the test harness times out the test. + if (logContents != "post-update\n") { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the post update " + + "process to create the expected contents in the post update log. Path: " + + postUpdateLog.path); + } + do_execute_soon(checkPostUpdateAppLog); + return; + } + Assert.ok(true, "the post update log contents" + MSG_SHOULD_EQUAL); + } + + do_execute_soon(checkPostUpdateAppLogFinished); +} + +/** + * Helper function to check if a file is in use on Windows by making a copy of + * a file and attempting to delete the original file. If the deletion is + * successful the copy of the original file is renamed to the original file's + * name and if the deletion is not successful the copy of the original file is + * deleted. + * + * @param aFile + * An nsIFile for the file to be checked if it is in use. + * @return true if the file can't be deleted and false otherwise. + */ +function isFileInUse(aFile) { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + if (!aFile.exists()) { + debugDump("file does not exist, path: " + aFile.path); + return false; + } + + let fileBak = aFile.parent; + fileBak.append(aFile.leafName + ".bak"); + try { + if (fileBak.exists()) { + fileBak.remove(false); + } + aFile.copyTo(aFile.parent, fileBak.leafName); + aFile.remove(false); + fileBak.moveTo(aFile.parent, aFile.leafName); + debugDump("file is not in use, path: " + aFile.path); + return false; + } catch (e) { + debugDump("file in use, path: " + aFile.path + ", exception: " + e); + try { + if (fileBak.exists()) { + fileBak.remove(false); + } + } catch (ex) { + logTestInfo("unable to remove backup file, path: " + + fileBak.path + ", exception: " + ex); + } + } + return true; +} + +/** + * Waits until files that are in use that break tests are no longer in use and + * then calls doTestFinish to end the test. + */ +function waitForFilesInUse() { + if (IS_WIN) { + let fileNames = [FILE_APP_BIN, FILE_UPDATER_BIN, + FILE_MAINTENANCE_SERVICE_INSTALLER_BIN]; + for (let i = 0; i < fileNames.length; ++i) { + let file = getApplyDirFile(fileNames[i], true); + if (isFileInUse(file)) { + do_timeout(FILE_IN_USE_TIMEOUT_MS, waitForFilesInUse); + return; + } + } + } + + debugDump("calling doTestFinish"); + doTestFinish(); +} + +/** + * Helper function for updater binary tests for verifying there are no update + * backup files left behind after an update. + * + * @param aFile + * An nsIFile to check if it has moz-backup for its extension. + */ +function checkForBackupFiles(aFile) { + Assert.notEqual(getFileExtension(aFile), "moz-backup", + "the file's extension should not equal moz-backup" + + getMsgPath(aFile.path)); +} + +/** + * Helper function for updater binary tests for recursively enumerating a + * directory and calling a callback function with the file as a parameter for + * each file found. + * + * @param aDir + * A nsIFile for the directory to be deleted + * @param aCallback + * A callback function that will be called with the file as a + * parameter for each file found. + */ +function checkFilesInDirRecursive(aDir, aCallback) { + if (!aDir.exists()) { + do_throw("Directory must exist!"); + } + + let dirEntries = aDir.directoryEntries; + while (dirEntries.hasMoreElements()) { + let entry = dirEntries.getNext().QueryInterface(Ci.nsIFile); + + if (entry.exists()) { + if (entry.isDirectory()) { + checkFilesInDirRecursive(entry, aCallback); + } else { + aCallback(entry); + } + } + } +} + + +/** + * Helper function to override the update prompt component to verify whether it + * is called or not. + * + * @param aCallback + * The callback to call if the update prompt component is called. + */ +function overrideUpdatePrompt(aCallback) { + Cu.import("resource://testing-common/MockRegistrar.jsm"); + MockRegistrar.register("@mozilla.org/updates/update-prompt;1", UpdatePrompt, [aCallback]); +} + +function UpdatePrompt(aCallback) { + this._callback = aCallback; + + let fns = ["checkForUpdates", "showUpdateAvailable", "showUpdateDownloaded", + "showUpdateError", "showUpdateHistory", "showUpdateInstalled"]; + + fns.forEach(function UP_fns(aPromptFn) { + UpdatePrompt.prototype[aPromptFn] = function() { + if (!this._callback) { + return; + } + + let callback = this._callback[aPromptFn]; + if (!callback) { + return; + } + + callback.apply(this._callback, + Array.prototype.slice.call(arguments)); + }; + }); +} + +UpdatePrompt.prototype = { + flags: Ci.nsIClassInfo.SINGLETON, + getScriptableHelper: () => null, + getInterfaces: function(aCount) { + let interfaces = [Ci.nsISupports, Ci.nsIUpdatePrompt]; + aCount.value = interfaces.length; + return interfaces; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIClassInfo, Ci.nsIUpdatePrompt]) +}; + +/* Update check listener */ +const updateCheckListener = { + onProgress: function UCL_onProgress(aRequest, aPosition, aTotalSize) { + }, + + onCheckComplete: function UCL_onCheckComplete(aRequest, aUpdates, aUpdateCount) { + gRequestURL = aRequest.channel.originalURI.spec; + gUpdateCount = aUpdateCount; + gUpdates = aUpdates; + debugDump("url = " + gRequestURL + ", " + + "request.status = " + aRequest.status + ", " + + "updateCount = " + aUpdateCount); + // Use a timeout to allow the XHR to complete + do_execute_soon(gCheckFunc); + }, + + onError: function UCL_onError(aRequest, aUpdate) { + gRequestURL = aRequest.channel.originalURI.spec; + gStatusCode = aRequest.status; + if (gStatusCode == 0) { + gStatusCode = aRequest.channel.QueryInterface(Ci.nsIRequest).status; + } + gStatusText = aUpdate.statusText ? aUpdate.statusText : null; + debugDump("url = " + gRequestURL + ", " + + "request.status = " + gStatusCode + ", " + + "update.statusText = " + gStatusText); + // Use a timeout to allow the XHR to complete + do_execute_soon(gCheckFunc.bind(null, aRequest, aUpdate)); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateCheckListener]) +}; + +/* Update download listener - nsIRequestObserver */ +const downloadListener = { + onStartRequest: function DL_onStartRequest(aRequest, aContext) { + }, + + onProgress: function DL_onProgress(aRequest, aContext, aProgress, aMaxProgress) { + }, + + onStatus: function DL_onStatus(aRequest, aContext, aStatus, aStatusText) { + }, + + onStopRequest: function DL_onStopRequest(aRequest, aContext, aStatus) { + gStatusResult = aStatus; + // Use a timeout to allow the request to complete + do_execute_soon(gCheckFunc); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver, + Ci.nsIProgressEventSink]) +}; + +/** + * Helper for starting the http server used by the tests + */ +function start_httpserver() { + let dir = getTestDirFile(); + debugDump("http server directory path: " + dir.path); + + if (!dir.isDirectory()) { + do_throw("A file instead of a directory was specified for HttpServer " + + "registerDirectory! Path: " + dir.path); + } + + let { HttpServer } = Cu.import("resource://testing-common/httpd.js", {}); + gTestserver = new HttpServer(); + gTestserver.registerDirectory("/", dir); + gTestserver.registerPathHandler("/" + gHTTPHandlerPath, pathHandler); + gTestserver.start(-1); + let testserverPort = gTestserver.identity.primaryPort; + gURLData = URL_HOST + ":" + testserverPort + "/"; + debugDump("http server port = " + testserverPort); +} + +/** + * Custom path handler for the http server + * + * @param aMetadata + * The http metadata for the request. + * @param aResponse + * The http response for the request. + */ +function pathHandler(aMetadata, aResponse) { + aResponse.setHeader("Content-Type", "text/xml", false); + aResponse.setStatusLine(aMetadata.httpVersion, gResponseStatusCode, "OK"); + aResponse.bodyOutputStream.write(gResponseBody, gResponseBody.length); +} + +/** + * Helper for stopping the http server used by the tests + * + * @param aCallback + * The callback to call after stopping the http server. + */ +function stop_httpserver(aCallback) { + Assert.ok(!!aCallback, "the aCallback parameter should be defined"); + gTestserver.stop(aCallback); +} + +/** + * Creates an nsIXULAppInfo + * + * @param aID + * The ID of the test application + * @param aName + * A name for the test application + * @param aVersion + * The version of the application + * @param aPlatformVersion + * The gecko version of the application + */ +function createAppInfo(aID, aName, aVersion, aPlatformVersion) { + const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1"; + const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}"); + let ifaces = [Ci.nsIXULAppInfo, Ci.nsIXULRuntime]; + if (IS_WIN) { + ifaces.push(Ci.nsIWinAppHelper); + } + const XULAppInfo = { + vendor: APP_INFO_VENDOR, + name: aName, + ID: aID, + version: aVersion, + appBuildID: "2007010101", + platformVersion: aPlatformVersion, + platformBuildID: "2007010101", + inSafeMode: false, + logConsoleErrors: true, + OS: "XPCShell", + XPCOMABI: "noarch-spidermonkey", + + QueryInterface: XPCOMUtils.generateQI(ifaces) + }; + + const XULAppInfoFactory = { + createInstance: function(aOuter, aIID) { + if (aOuter == null) { + return XULAppInfo.QueryInterface(aIID); + } + throw Cr.NS_ERROR_NO_AGGREGATION; + } + }; + + let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo", + XULAPPINFO_CONTRACTID, XULAppInfoFactory); +} + +/** + * Returns the platform specific arguments used by nsIProcess when launching + * the application. + * + * @param aExtraArgs (optional) + * An array of extra arguments to append to the default arguments. + * @return an array of arguments to be passed to nsIProcess. + * + * Note: a shell is necessary to pipe the application's console output which + * would otherwise pollute the xpcshell log. + * + * Command line arguments used when launching the application: + * -no-remote prevents shell integration from being affected by an existing + * application process. + * -test-process-updates makes the application exit after being relaunched by + * the updater. + * the platform specific string defined by PIPE_TO_NULL to output both stdout + * and stderr to null. This is needed to prevent output from the application + * from ending up in the xpchsell log. + */ +function getProcessArgs(aExtraArgs) { + if (!aExtraArgs) { + aExtraArgs = []; + } + + let appBinPath = getApplyDirFile(DIR_MACOS + FILE_APP_BIN, false).path; + if (/ /.test(appBinPath)) { + appBinPath = '"' + appBinPath + '"'; + } + + let args; + if (IS_UNIX) { + let launchScript = getLaunchScript(); + // Precreate the script with executable permissions + launchScript.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_DIRECTORY); + + let scriptContents = "#! /bin/sh\n"; + scriptContents += appBinPath + " -no-remote -test-process-updates " + + aExtraArgs.join(" ") + " " + PIPE_TO_NULL; + writeFile(launchScript, scriptContents); + debugDump("created " + launchScript.path + " containing:\n" + + scriptContents); + args = [launchScript.path]; + } else { + args = ["/D", "/Q", "/C", appBinPath, "-no-remote", "-test-process-updates"]. + concat(aExtraArgs).concat([PIPE_TO_NULL]); + } + return args; +} + +/** + * Gets a file path for the application to dump its arguments into. This is used + * to verify that a callback application is launched. + * + * @return the file for the application to dump its arguments into. + */ +function getAppArgsLogPath() { + let appArgsLog = do_get_file("/" + gTestID + "_app_args_log", true); + if (appArgsLog.exists()) { + appArgsLog.remove(false); + } + let appArgsLogPath = appArgsLog.path; + if (/ /.test(appArgsLogPath)) { + appArgsLogPath = '"' + appArgsLogPath + '"'; + } + return appArgsLogPath; +} + +/** + * Gets the nsIFile reference for the shell script to launch the application. If + * the file exists it will be removed by this function. + * + * @return the nsIFile for the shell script to launch the application. + */ +function getLaunchScript() { + let launchScript = do_get_file("/" + gTestID + "_launch.sh", true); + if (launchScript.exists()) { + launchScript.remove(false); + } + return launchScript; +} + +/** + * Makes GreD, XREExeF, and UpdRootD point to unique file system locations so + * xpcshell tests can run in parallel and to keep the environment clean. + */ +function adjustGeneralPaths() { + let dirProvider = { + getFile: function AGP_DP_getFile(aProp, aPersistent) { + aPersistent.value = true; + switch (aProp) { + case NS_GRE_DIR: + if (gUseTestAppDir) { + return getApplyDirFile(DIR_RESOURCES, true); + } + break; + case NS_GRE_BIN_DIR: + if (gUseTestAppDir) { + return getApplyDirFile(DIR_MACOS, true); + } + break; + case XRE_EXECUTABLE_FILE: + if (gUseTestAppDir) { + return getApplyDirFile(DIR_MACOS + FILE_APP_BIN, true); + } + break; + case XRE_UPDATE_ROOT_DIR: + return getMockUpdRootD(); + } + return null; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]) + }; + let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService); + ds.QueryInterface(Ci.nsIProperties).undefine(NS_GRE_DIR); + ds.QueryInterface(Ci.nsIProperties).undefine(NS_GRE_BIN_DIR); + ds.QueryInterface(Ci.nsIProperties).undefine(XRE_EXECUTABLE_FILE); + ds.registerProvider(dirProvider); + do_register_cleanup(function AGP_cleanup() { + debugDump("start - unregistering directory provider"); + + if (gAppTimer) { + debugDump("start - cancel app timer"); + gAppTimer.cancel(); + gAppTimer = null; + debugDump("finish - cancel app timer"); + } + + if (gProcess && gProcess.isRunning) { + debugDump("start - kill process"); + try { + gProcess.kill(); + } catch (e) { + debugDump("kill process failed. Exception: " + e); + } + gProcess = null; + debugDump("finish - kill process"); + } + + if (gHandle) { + try { + debugDump("start - closing handle"); + let kernel32 = ctypes.open("kernel32"); + let CloseHandle = kernel32.declare("CloseHandle", ctypes.default_abi, + ctypes.bool, /* return*/ + ctypes.voidptr_t /* handle*/); + if (!CloseHandle(gHandle)) { + debugDump("call to CloseHandle failed"); + } + kernel32.close(); + gHandle = null; + debugDump("finish - closing handle"); + } catch (e) { + debugDump("call to CloseHandle failed. Exception: " + e); + } + } + + // Call end_test first before the directory provider is unregistered + if (typeof end_test == typeof Function) { + debugDump("calling end_test"); + end_test(); + } + + ds.unregisterProvider(dirProvider); + cleanupTestCommon(); + + debugDump("finish - unregistering directory provider"); + }); +} + +/** + * The timer callback to kill the process if it takes too long. + */ +const gAppTimerCallback = { + notify: function TC_notify(aTimer) { + gAppTimer = null; + if (gProcess.isRunning) { + logTestInfo("attempting to kill process"); + gProcess.kill(); + } + Assert.ok(false, "launch application timer expired"); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback]) +}; + +/** + * Launches an application to apply an update. + */ +function runUpdateUsingApp(aExpectedStatus) { + /** + * The observer for the call to nsIProcess:runAsync. When completed + * runUpdateFinished will be called. + */ + const processObserver = { + observe: function PO_observe(aSubject, aTopic, aData) { + debugDump("topic: " + aTopic + ", process exitValue: " + + gProcess.exitValue); + resetEnvironment(); + if (gAppTimer) { + gAppTimer.cancel(); + gAppTimer = null; + } + Assert.equal(gProcess.exitValue, 0, + "the application process exit value should be 0"); + Assert.equal(aTopic, "process-finished", + "the application process observer topic should be " + + "process-finished"); + + if (IS_SERVICE_TEST) { + waitForServiceStop(false); + } + + do_execute_soon(afterAppExits); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + }; + + function afterAppExits() { + gTimeoutRuns++; + + if (IS_WIN) { + waitForApplicationStop(FILE_UPDATER_BIN); + } + + let status; + try { + status = readStatusFile(); + } catch (e) { + logTestInfo("error reading status file, exception: " + e); + } + // Don't proceed until the update's status is the expected value. + if (status != aExpectedStatus) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + logUpdateLog(FILE_UPDATE_LOG); + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update " + + "status to equal: " + + aExpectedStatus + + ", current status: " + status); + } else { + do_timeout(FILE_IN_USE_TIMEOUT_MS, afterAppExits); + } + return; + } + + // Don't check for an update log when the code in nsUpdateDriver.cpp skips + // updating. + if (aExpectedStatus != STATE_PENDING && + aExpectedStatus != STATE_PENDING_SVC && + aExpectedStatus != STATE_APPLIED && + aExpectedStatus != STATE_APPLIED_SVC) { + // Don't proceed until the update log has been created. + let log = getUpdateLog(FILE_UPDATE_LOG); + if (!log.exists()) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update " + + "log to be created. Path: " + log.path); + } + do_timeout(FILE_IN_USE_TIMEOUT_MS, afterAppExits); + return; + } + } + + do_execute_soon(runUpdateFinished); + } + + debugDump("start - launching application to apply update"); + + let appBin = getApplyDirFile(DIR_MACOS + FILE_APP_BIN, false); + + let launchBin = getLaunchBin(); + let args = getProcessArgs(); + debugDump("launching " + launchBin.path + " " + args.join(" ")); + + gProcess = Cc["@mozilla.org/process/util;1"]. + createInstance(Ci.nsIProcess); + gProcess.init(launchBin); + + gAppTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gAppTimer.initWithCallback(gAppTimerCallback, APP_TIMER_TIMEOUT, + Ci.nsITimer.TYPE_ONE_SHOT); + + setEnvironment(); + debugDump("launching application"); + gProcess.runAsync(args, args.length, processObserver); + + debugDump("finish - launching application to apply update"); +} + +/** + * Sets the environment that will be used by the application process when it is + * launched. + */ +function setEnvironment() { + // Prevent setting the environment more than once. + if (gShouldResetEnv !== undefined) { + return; + } + + gShouldResetEnv = true; + + // See bug 1279108. + if (gEnv.exists("ASAN_OPTIONS")) { + gASanOptions = gEnv.get("ASAN_OPTIONS"); + gEnv.set("ASAN_OPTIONS", gASanOptions + ":detect_leaks=0"); + } else { + gEnv.set("ASAN_OPTIONS", "detect_leaks=0"); + } + + if (IS_WIN && !gEnv.exists("XRE_NO_WINDOWS_CRASH_DIALOG")) { + gAddedEnvXRENoWindowsCrashDialog = true; + debugDump("setting the XRE_NO_WINDOWS_CRASH_DIALOG environment " + + "variable to 1... previously it didn't exist"); + gEnv.set("XRE_NO_WINDOWS_CRASH_DIALOG", "1"); + } + + if (IS_UNIX) { + let appGreBinDir = gGREBinDirOrig.clone(); + let envGreBinDir = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsILocalFile); + let shouldSetEnv = true; + if (IS_MACOSX) { + if (gEnv.exists("DYLD_LIBRARY_PATH")) { + gEnvDyldLibraryPath = gEnv.get("DYLD_LIBRARY_PATH"); + envGreBinDir.initWithPath(gEnvDyldLibraryPath); + if (envGreBinDir.path == appGreBinDir.path) { + gEnvDyldLibraryPath = null; + shouldSetEnv = false; + } + } + + if (shouldSetEnv) { + debugDump("setting DYLD_LIBRARY_PATH environment variable value to " + + appGreBinDir.path); + gEnv.set("DYLD_LIBRARY_PATH", appGreBinDir.path); + } + } else { + if (gEnv.exists("LD_LIBRARY_PATH")) { + gEnvLdLibraryPath = gEnv.get("LD_LIBRARY_PATH"); + envGreBinDir.initWithPath(gEnvLdLibraryPath); + if (envGreBinDir.path == appGreBinDir.path) { + gEnvLdLibraryPath = null; + shouldSetEnv = false; + } + } + + if (shouldSetEnv) { + debugDump("setting LD_LIBRARY_PATH environment variable value to " + + appGreBinDir.path); + gEnv.set("LD_LIBRARY_PATH", appGreBinDir.path); + } + } + } + + if (gEnv.exists("XPCOM_MEM_LEAK_LOG")) { + gEnvXPCOMMemLeakLog = gEnv.get("XPCOM_MEM_LEAK_LOG"); + debugDump("removing the XPCOM_MEM_LEAK_LOG environment variable... " + + "previous value " + gEnvXPCOMMemLeakLog); + gEnv.set("XPCOM_MEM_LEAK_LOG", ""); + } + + if (gEnv.exists("XPCOM_DEBUG_BREAK")) { + gEnvXPCOMDebugBreak = gEnv.get("XPCOM_DEBUG_BREAK"); + debugDump("setting the XPCOM_DEBUG_BREAK environment variable to " + + "warn... previous value " + gEnvXPCOMDebugBreak); + } else { + debugDump("setting the XPCOM_DEBUG_BREAK environment variable to " + + "warn... previously it didn't exist"); + } + + gEnv.set("XPCOM_DEBUG_BREAK", "warn"); + + if (IS_SERVICE_TEST) { + debugDump("setting MOZ_NO_SERVICE_FALLBACK environment variable to 1"); + gEnv.set("MOZ_NO_SERVICE_FALLBACK", "1"); + } +} + +/** + * Sets the environment back to the original values after launching the + * application. + */ +function resetEnvironment() { + // Prevent resetting the environment more than once. + if (gShouldResetEnv !== true) { + return; + } + + gShouldResetEnv = false; + + // Restore previous ASAN_OPTIONS if there were any. + gEnv.set("ASAN_OPTIONS", gASanOptions ? gASanOptions : ""); + + if (gEnvXPCOMMemLeakLog) { + debugDump("setting the XPCOM_MEM_LEAK_LOG environment variable back to " + + gEnvXPCOMMemLeakLog); + gEnv.set("XPCOM_MEM_LEAK_LOG", gEnvXPCOMMemLeakLog); + } + + if (gEnvXPCOMDebugBreak) { + debugDump("setting the XPCOM_DEBUG_BREAK environment variable back to " + + gEnvXPCOMDebugBreak); + gEnv.set("XPCOM_DEBUG_BREAK", gEnvXPCOMDebugBreak); + } else if (gEnv.exists("XPCOM_DEBUG_BREAK")) { + debugDump("clearing the XPCOM_DEBUG_BREAK environment variable"); + gEnv.set("XPCOM_DEBUG_BREAK", ""); + } + + if (IS_UNIX) { + if (IS_MACOSX) { + if (gEnvDyldLibraryPath) { + debugDump("setting DYLD_LIBRARY_PATH environment variable value " + + "back to " + gEnvDyldLibraryPath); + gEnv.set("DYLD_LIBRARY_PATH", gEnvDyldLibraryPath); + } else if (gEnvDyldLibraryPath !== null) { + debugDump("removing DYLD_LIBRARY_PATH environment variable"); + gEnv.set("DYLD_LIBRARY_PATH", ""); + } + } else if (gEnvLdLibraryPath) { + debugDump("setting LD_LIBRARY_PATH environment variable value back " + + "to " + gEnvLdLibraryPath); + gEnv.set("LD_LIBRARY_PATH", gEnvLdLibraryPath); + } else if (gEnvLdLibraryPath !== null) { + debugDump("removing LD_LIBRARY_PATH environment variable"); + gEnv.set("LD_LIBRARY_PATH", ""); + } + } + + if (IS_WIN && gAddedEnvXRENoWindowsCrashDialog) { + debugDump("removing the XRE_NO_WINDOWS_CRASH_DIALOG environment " + + "variable"); + gEnv.set("XRE_NO_WINDOWS_CRASH_DIALOG", ""); + } + + if (IS_SERVICE_TEST) { + debugDump("removing MOZ_NO_SERVICE_FALLBACK environment variable"); + gEnv.set("MOZ_NO_SERVICE_FALLBACK", ""); + } +} diff --git a/toolkit/mozapps/update/tests/moz.build b/toolkit/mozapps/update/tests/moz.build new file mode 100644 index 000000000..842ec7f90 --- /dev/null +++ b/toolkit/mozapps/update/tests/moz.build @@ -0,0 +1,101 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +HAS_MISC_RULE = True + +FINAL_TARGET = '_tests/xpcshell/toolkit/mozapps/update/tests/data' + +MOCHITEST_CHROME_MANIFESTS += ['chrome/chrome.ini'] + +XPCSHELL_TESTS_MANIFESTS += [ + 'unit_aus_update/xpcshell.ini', + 'unit_base_updater/xpcshell.ini' +] + +if CONFIG['MOZ_MAINTENANCE_SERVICE']: + XPCSHELL_TESTS_MANIFESTS += ['unit_service_updater/xpcshell.ini'] + +SimplePrograms([ + 'TestAUSHelper', + 'TestAUSReadStrings', +]) + +LOCAL_INCLUDES += [ + '/toolkit/mozapps/update', + '/toolkit/mozapps/update/common', +] + +if CONFIG['OS_ARCH'] == 'WINNT': + USE_LIBS += [ + 'updatecommon-standalone', + ] + + OS_LIBS += [ + 'shlwapi', + ] +else: + USE_LIBS += [ + 'updatecommon', + ] + +for var in ('MOZ_APP_NAME', 'MOZ_APP_BASENAME', 'MOZ_APP_DISPLAYNAME', + 'MOZ_APP_VENDOR', 'BIN_SUFFIX', 'MOZ_DEBUG'): + DEFINES[var] = CONFIG[var] + +DEFINES['NS_NO_XPCOM'] = True + +if CONFIG['MOZ_MAINTENANCE_SERVICE']: + DEFINES['MOZ_MAINTENANCE_SERVICE'] = CONFIG['MOZ_MAINTENANCE_SERVICE'] + +# For debugging purposes only +#DEFINES['DISABLE_UPDATER_AUTHENTICODE_CHECK'] = True + +if CONFIG['OS_ARCH'] == 'WINNT': + DEFINES['UNICODE'] = True + DEFINES['_UNICODE'] = True + USE_STATIC_LIBS = True + if CONFIG['GNU_CC']: + WIN32_EXE_LDFLAGS += ['-municode'] + +TEST_HARNESS_FILES.testing.mochitest.chrome.toolkit.mozapps.update.tests.data += [ + 'data/shared.js', + 'data/sharedUpdateXML.js', + 'data/simple.mar', +] + +FINAL_TARGET_FILES += [ + 'data/complete.exe', + 'data/complete.mar', + 'data/complete.png', + 'data/complete_log_success_mac', + 'data/complete_log_success_win', + 'data/complete_mac.mar', + 'data/complete_precomplete', + 'data/complete_precomplete_mac', + 'data/complete_removed-files', + 'data/complete_removed-files_mac', + 'data/complete_update_manifest', + 'data/old_version.mar', + 'data/partial.exe', + 'data/partial.mar', + 'data/partial.png', + 'data/partial_log_failure_mac', + 'data/partial_log_failure_win', + 'data/partial_log_success_mac', + 'data/partial_log_success_win', + 'data/partial_mac.mar', + 'data/partial_precomplete', + 'data/partial_precomplete_mac', + 'data/partial_removed-files', + 'data/partial_removed-files_mac', + 'data/partial_update_manifest', + 'data/replace_log_success', + 'data/shared.js', + 'data/sharedUpdateXML.js', + 'data/simple.mar', + 'data/wrong_product_channel.mar', + 'data/xpcshellUtilsAUS.js', +] diff --git a/toolkit/mozapps/update/tests/unit_aus_update/.eslintrc.js b/toolkit/mozapps/update/tests/unit_aus_update/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js b/toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js new file mode 100644 index 000000000..1985df959 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js @@ -0,0 +1,138 @@ +/* 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/. + */ + +function run_test() { + setupTestCommon(); + + // Verify write access to the custom app dir + debugDump("testing write access to the application directory"); + let testFile = getCurrentProcessDir(); + testFile.append("update_write_access_test"); + testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + Assert.ok(testFile.exists(), MSG_SHOULD_EXIST); + testFile.remove(false); + Assert.ok(!testFile.exists(), MSG_SHOULD_NOT_EXIST); + + standardInit(); + + if (IS_WIN) { + // Create a mutex to prevent being able to check for or apply updates. + debugDump("attempting to create mutex"); + let handle = createMutex(getPerInstallationMutexName()); + Assert.ok(!!handle, "the update mutex should have been created"); + + // Check if available updates cannot be checked for when there is a mutex + // for this installation. + Assert.ok(!gAUS.canCheckForUpdates, "should not be able to check for " + + "updates when there is an update mutex"); + + // Check if updates cannot be applied when there is a mutex for this + // installation. + Assert.ok(!gAUS.canApplyUpdates, "should not be able to apply updates " + + "when there is an update mutex"); + + debugDump("destroying mutex"); + closeHandle(handle); + } + + // Check if available updates can be checked for + Assert.ok(gAUS.canCheckForUpdates, "should be able to check for updates"); + // Check if updates can be applied + Assert.ok(gAUS.canApplyUpdates, "should be able to apply updates"); + + if (IS_WIN) { + // Attempt to create a mutex when application update has already created one + // with the same name. + debugDump("attempting to create mutex"); + let handle = createMutex(getPerInstallationMutexName()); + + Assert.ok(!handle, "should not be able to create the update mutex when " + + "the application has created the update mutex"); + } + + doTestFinish(); +} + +/** + * Determines a unique mutex name for the installation. + * + * @return Global mutex path. + */ +function getPerInstallationMutexName() { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + let hasher = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + + let exeFile = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsILocalFile); + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + let data = converter.convertToByteArray(exeFile.path.toLowerCase()); + + hasher.update(data, data.length); + return "Global\\MozillaUpdateMutex-" + hasher.finish(true); +} + +/** + * Closes a Win32 handle. + * + * @param aHandle + * The handle to close. + */ +function closeHandle(aHandle) { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + let lib = ctypes.open("kernel32.dll"); + let CloseHandle = lib.declare("CloseHandle", + ctypes.winapi_abi, + ctypes.int32_t, /* success */ + ctypes.void_t.ptr); /* handle */ + CloseHandle(aHandle); + lib.close(); +} + +/** + * Creates a mutex. + * + * @param aName + * The name for the mutex. + * @return The Win32 handle to the mutex. + */ +function createMutex(aName) { + if (!IS_WIN) { + do_throw("Windows only function called by a different platform!"); + } + + const INITIAL_OWN = 1; + const ERROR_ALREADY_EXISTS = 0xB7; + let lib = ctypes.open("kernel32.dll"); + let CreateMutexW = lib.declare("CreateMutexW", + ctypes.winapi_abi, + ctypes.void_t.ptr, /* return handle */ + ctypes.void_t.ptr, /* security attributes */ + ctypes.int32_t, /* initial owner */ + ctypes.char16_t.ptr); /* name */ + + let handle = CreateMutexW(null, INITIAL_OWN, aName); + lib.close(); + let alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS; + if (handle && !handle.isNull() && alreadyExists) { + closeHandle(handle); + handle = null; + } + + if (handle && handle.isNull()) { + handle = null; + } + + return handle; +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js new file mode 100644 index 000000000..a0a95af1b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js @@ -0,0 +1,46 @@ +/* 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/. + */ + +/* General Update Manager Tests */ + +function run_test() { + setupTestCommon(); + + debugDump("testing removal of an active update for a channel that is not" + + "valid due to switching channels (Bug 486275)."); + + let patches = getLocalPatchString(null, null, null, null, null, null, + STATE_DOWNLOADING); + let updates = getLocalUpdateString(patches, null, null, "version 1.0", "1.0"); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_DOWNLOADING); + + patches = getLocalPatchString(null, null, null, null, null, null, + STATE_FAILED); + updates = getLocalUpdateString(patches, null, "Existing", "version 3.0", + "3.0", "3.0", null, null, null, null, + getString("patchApplyFailure")); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), false); + + setUpdateChannel("original_channel"); + + standardInit(); + + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager update count" + MSG_SHOULD_EQUAL); + let update = gUpdateManager.getUpdateAt(0); + Assert.equal(update.name, "Existing", + "the update's name" + MSG_SHOULD_EQUAL); + + Assert.ok(!gUpdateManager.activeUpdate, + "there should not be an active update"); + // Verify that the active-update.xml file has had the update from the old + // channel removed. + let file = getUpdatesXMLFile(true); + Assert.equal(readFile(file), getLocalUpdatesXMLString(""), + "the contents of active-update.xml" + MSG_SHOULD_EQUAL); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js new file mode 100644 index 000000000..fc4f09787 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js @@ -0,0 +1,29 @@ +/* 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/. + */ + + +function run_test() { + setupTestCommon(); + + debugDump("testing cleanup of an update download in progress for an " + + "older version of the application on startup (Bug 485624)"); + + let patches = getLocalPatchString(null, null, null, null, null, null, + STATE_DOWNLOADING); + let updates = getLocalUpdateString(patches, null, null, "version 0.9", "0.9"); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_DOWNLOADING); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + + standardInit(); + + Assert.ok(!gUpdateManager.activeUpdate, + "there should not be an active update"); + Assert.equal(gUpdateManager.updateCount, 0, + "the update manager update count" + MSG_SHOULD_EQUAL); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js new file mode 100644 index 000000000..b2d8ecbc6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js @@ -0,0 +1,30 @@ +/* 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/. + */ + +function run_test() { + setupTestCommon(); + + debugDump("testing removal of an update download in progress for the " + + "same version of the application with the same application " + + "build id on startup (Bug 536547)"); + + let patches = getLocalPatchString(null, null, null, null, null, null, + STATE_DOWNLOADING); + let updates = getLocalUpdateString(patches, null, null, "version 1.0", "1.0", + "2007010101"); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_DOWNLOADING); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + + standardInit(); + + Assert.ok(!gUpdateManager.activeUpdate, + "there should not be an active update"); + Assert.equal(gUpdateManager.updateCount, 0, + "the update manager update count" + MSG_SHOULD_EQUAL); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js new file mode 100644 index 000000000..13e4aeaf6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function run_test() { + setupTestCommon(); + + debugDump("testing update cleanup when reading the status file returns " + + "STATUS_NONE and the update xml has an update with " + + "STATE_DOWNLOADING (Bug 539717)."); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + let patches = getLocalPatchString(null, null, null, null, null, null, + STATE_DOWNLOADING); + let updates = getLocalUpdateString(patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_NONE); + + standardInit(); + + let dir = getUpdatesDir(); + dir.append(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + let statusFile = dir.clone(); + statusFile.append(FILE_UPDATE_STATUS); + Assert.ok(!statusFile.exists(), MSG_SHOULD_NOT_EXIST); + + Assert.ok(!gUpdateManager.activeUpdate, + "there should not be an active update"); + Assert.equal(gUpdateManager.updateCount, 0, + "the update manager update count" + MSG_SHOULD_EQUAL); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js new file mode 100644 index 000000000..7661da82d --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function run_test() { + setupTestCommon(); + + debugDump("testing update cleanup when reading the status file returns " + + "STATUS_NONE, the version file is for a newer version, and the " + + "update xml has an update with STATE_PENDING (Bug 601701)."); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + let patches = getLocalPatchString(null, null, null, null, null, null, + STATE_PENDING); + let updates = getLocalUpdateString(patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeVersionFile("99.9"); + + standardInit(); + + // Check that there is no activeUpdate first so the updates directory is + // cleaned up by the UpdateManager before the remaining tests. + Assert.ok(!gUpdateManager.activeUpdate, + "there should not be an active update"); + Assert.equal(gUpdateManager.updateCount, 0, + "the update manager update count" + MSG_SHOULD_EQUAL); + + let dir = getUpdatesDir(); + dir.append(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + let versionFile = dir.clone(); + versionFile.append(FILE_UPDATE_VERSION); + Assert.ok(!versionFile.exists(), MSG_SHOULD_NOT_EXIST); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js new file mode 100644 index 000000000..d683b9931 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js @@ -0,0 +1,37 @@ +/* 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/. + */ + +function run_test() { + setupTestCommon(); + + debugDump("testing that the update.log is moved after a successful update"); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + let patches = getLocalPatchString(null, null, null, null, null, null, + STATE_PENDING); + let updates = getLocalUpdateString(patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_SUCCEEDED); + + let log = getUpdateLog(FILE_UPDATE_LOG); + writeFile(log, "Last Update Log"); + + standardInit(); + + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST); + + log = getUpdateLog(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST); + Assert.equal(readFile(log), "Last Update Log", + "the last update log contents" + MSG_SHOULD_EQUAL); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST); + + let dir = getUpdatesPatchDir(); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js new file mode 100644 index 000000000..8be93d0ff --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js @@ -0,0 +1,45 @@ +/* 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/. + */ + +function run_test() { + setupTestCommon(); + + debugDump("testing update logs are first in first out deleted"); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + let patches = getLocalPatchString(null, null, null, null, null, null, + STATE_PENDING); + let updates = getLocalUpdateString(patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_SUCCEEDED); + + let log = getUpdateLog(FILE_LAST_UPDATE_LOG); + writeFile(log, "Backup Update Log"); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + writeFile(log, "To Be Deleted Backup Update Log"); + + log = getUpdateLog(FILE_UPDATE_LOG); + writeFile(log, "Last Update Log"); + + standardInit(); + + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST); + + log = getUpdateLog(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST); + Assert.equal(readFile(log), "Last Update Log", + "the last update log contents" + MSG_SHOULD_EQUAL); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST); + Assert.equal(readFile(log), "Backup Update Log", + "the backup update log contents" + MSG_SHOULD_EQUAL); + + let dir = getUpdatesPatchDir(); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadAndHashCheckMar.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadAndHashCheckMar.js new file mode 100644 index 000000000..b715fb56e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadAndHashCheckMar.js @@ -0,0 +1,161 @@ +/* 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/. + */ + +var gNextRunFunc; +var gExpectedStatusResult; + +function run_test() { + // The network code that downloads the mar file accesses the profile to cache + // the download, but the profile is only available after calling + // do_get_profile in xpcshell tests. This prevents an error from being logged. + do_get_profile(); + + setupTestCommon(); + + debugDump("testing mar download and mar hash verification"); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + standardInit(); + // Only perform the non hash check tests when mar signing is enabled since the + // update service doesn't perform hash checks when mar signing is enabled. + if (MOZ_VERIFY_MAR_SIGNATURE) { + do_execute_soon(run_test_pt11); + } else { + do_execute_soon(run_test_pt1); + } +} + +// The HttpServer must be stopped before calling do_test_finished +function finish_test() { + stop_httpserver(doTestFinish); +} + +// Helper function for testing mar downloads that have the correct size +// specified in the update xml. +function run_test_helper_pt1(aMsg, aExpectedStatusResult, aNextRunFunc) { + gUpdates = null; + gUpdateCount = null; + gStatusResult = null; + gCheckFunc = check_test_helper_pt1_1; + gNextRunFunc = aNextRunFunc; + gExpectedStatusResult = aExpectedStatusResult; + debugDump(aMsg, Components.stack.caller); + gUpdateChecker.checkForUpdates(updateCheckListener, true); +} + +function check_test_helper_pt1_1() { + Assert.equal(gUpdateCount, 1, + "the update count" + MSG_SHOULD_EQUAL); + gCheckFunc = check_test_helper_pt1_2; + let bestUpdate = gAUS.selectUpdate(gUpdates, gUpdateCount); + let state = gAUS.downloadUpdate(bestUpdate, false); + if (state == STATE_NONE || state == STATE_FAILED) { + do_throw("nsIApplicationUpdateService:downloadUpdate returned " + state); + } + gAUS.addDownloadListener(downloadListener); +} + +function check_test_helper_pt1_2() { + Assert.equal(gStatusResult, gExpectedStatusResult, + "the download status result" + MSG_SHOULD_EQUAL); + gAUS.removeDownloadListener(downloadListener); + gNextRunFunc(); +} + +function setResponseBody(aHashFunction, aHashValue, aSize) { + let patches = getRemotePatchString(null, null, + aHashFunction, aHashValue, aSize); + let updates = getRemoteUpdateString(patches); + gResponseBody = getRemoteUpdatesXMLString(updates); +} + +// mar download with a valid MD5 hash +function run_test_pt1() { + setResponseBody("MD5", MD5_HASH_SIMPLE_MAR); + run_test_helper_pt1("mar download with a valid MD5 hash", + Cr.NS_OK, run_test_pt2); +} + +// mar download with an invalid MD5 hash +function run_test_pt2() { + setResponseBody("MD5", MD5_HASH_SIMPLE_MAR + "0"); + run_test_helper_pt1("mar download with an invalid MD5 hash", + Cr.NS_ERROR_CORRUPTED_CONTENT, run_test_pt3); +} + +// mar download with a valid SHA1 hash +function run_test_pt3() { + setResponseBody("SHA1", SHA1_HASH_SIMPLE_MAR); + run_test_helper_pt1("mar download with a valid SHA1 hash", + Cr.NS_OK, run_test_pt4); +} + +// mar download with an invalid SHA1 hash +function run_test_pt4() { + setResponseBody("SHA1", SHA1_HASH_SIMPLE_MAR + "0"); + run_test_helper_pt1("mar download with an invalid SHA1 hash", + Cr.NS_ERROR_CORRUPTED_CONTENT, run_test_pt5); +} + +// mar download with a valid SHA256 hash +function run_test_pt5() { + setResponseBody("SHA256", SHA256_HASH_SIMPLE_MAR); + run_test_helper_pt1("mar download with a valid SHA256 hash", + Cr.NS_OK, run_test_pt6); +} + +// mar download with an invalid SHA256 hash +function run_test_pt6() { + setResponseBody("SHA256", SHA256_HASH_SIMPLE_MAR + "0"); + run_test_helper_pt1("mar download with an invalid SHA256 hash", + Cr.NS_ERROR_CORRUPTED_CONTENT, run_test_pt7); +} + +// mar download with a valid SHA384 hash +function run_test_pt7() { + setResponseBody("SHA384", SHA384_HASH_SIMPLE_MAR); + run_test_helper_pt1("mar download with a valid SHA384 hash", + Cr.NS_OK, run_test_pt8); +} + +// mar download with an invalid SHA384 hash +function run_test_pt8() { + setResponseBody("SHA384", SHA384_HASH_SIMPLE_MAR + "0"); + run_test_helper_pt1("mar download with an invalid SHA384 hash", + Cr.NS_ERROR_CORRUPTED_CONTENT, run_test_pt9); +} + +// mar download with a valid SHA512 hash +function run_test_pt9() { + setResponseBody("SHA512", SHA512_HASH_SIMPLE_MAR); + run_test_helper_pt1("mar download with a valid SHA512 hash", + Cr.NS_OK, run_test_pt10); +} + +// mar download with an invalid SHA512 hash +function run_test_pt10() { + setResponseBody("SHA512", SHA512_HASH_SIMPLE_MAR + "0"); + run_test_helper_pt1("mar download with an invalid SHA512 hash", + Cr.NS_ERROR_CORRUPTED_CONTENT, run_test_pt11); +} + +// mar download with the mar not found +function run_test_pt11() { + let patches = getRemotePatchString(null, gURLData + "missing.mar"); + let updates = getRemoteUpdateString(patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + run_test_helper_pt1("mar download with the mar not found", + Cr.NS_ERROR_UNEXPECTED, run_test_pt12); +} + +// mar download with a valid MD5 hash but invalid file size +function run_test_pt12() { + const arbitraryFileSize = 1024000; + setResponseBody("MD5", MD5_HASH_SIMPLE_MAR, arbitraryFileSize); + run_test_helper_pt1("mar download with a valid MD5 hash but invalid file size", + Cr.NS_ERROR_UNEXPECTED, finish_test); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadCompleteAfterPartialFailure.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadCompleteAfterPartialFailure.js new file mode 100644 index 000000000..159033792 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadCompleteAfterPartialFailure.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +Components.utils.import("resource://testing-common/MockRegistrar.jsm"); + +const WindowWatcher = { + getNewPrompter: function WW_getNewPrompter(aParent) { + Assert.ok(!aParent, + "the aParent parameter should not be defined"); + return { + alert: function WW_GNP_alert(aTitle, aText) { + let title = getString("updaterIOErrorTitle"); + Assert.equal(aTitle, title, + "the ui string for title" + MSG_SHOULD_EQUAL); + let text = gUpdateBundle.formatStringFromName("updaterIOErrorMsg", + [Services.appinfo.name, + Services.appinfo.name], 2); + Assert.equal(aText, text, + "the ui string for message" + MSG_SHOULD_EQUAL); + + doTestFinish(); + } + }; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher]) +}; + +function run_test() { + setupTestCommon(); + + debugDump("testing download a complete on partial failure. Calling " + + "nsIUpdatePrompt::showUpdateError should call getNewPrompter " + + "and alert on the object returned by getNewPrompter when the " + + "update.state == " + STATE_FAILED + " and the update.errorCode " + + "== " + WRITE_ERROR + " (Bug 595059)."); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false); + + let windowWatcherCID = + MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", + WindowWatcher); + do_register_cleanup(() => { + MockRegistrar.unregister(windowWatcherCID); + }); + + standardInit(); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + let url = URL_HOST + "/" + FILE_COMPLETE_MAR; + let patches = getLocalPatchString("complete", url, null, null, null, null, + STATE_FAILED); + let updates = getLocalUpdateString(patches, null, null, "version 1.0", "1.0", + null, null, null, null, url); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_FAILED); + + reloadUpdateManagerData(); + + let update = gUpdateManager.activeUpdate; + update.errorCode = WRITE_ERROR; + let prompter = Cc["@mozilla.org/updates/update-prompt;1"]. + createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateError(update); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js new file mode 100644 index 000000000..ef2da26af --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js @@ -0,0 +1,225 @@ +/* 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/. + */ + +/* General MAR File Download Tests */ + +Components.utils.import("resource://testing-common/MockRegistrar.jsm"); +const INC_CONTRACT_ID = "@mozilla.org/network/incremental-download;1"; + +// gIncrementalDownloadErrorType is used to loop through each of the connection +// error types in the Mock incremental downloader. +var gIncrementalDownloadErrorType = 0; + +var gNextRunFunc; +var gExpectedStatusResult; + +function run_test() { + setupTestCommon(); + + debugDump("testing mar downloads, mar hash verification, and " + + "mar download interrupted recovery"); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + standardInit(); + do_execute_soon(run_test_pt1); +} + +// The HttpServer must be stopped before calling do_test_finished +function finish_test() { + stop_httpserver(doTestFinish); +} + +// Helper function for testing mar downloads that have the correct size +// specified in the update xml. +function run_test_helper_pt1(aMsg, aExpectedStatusResult, aNextRunFunc) { + gUpdates = null; + gUpdateCount = null; + gStatusResult = null; + gCheckFunc = check_test_helper_pt1_1; + gNextRunFunc = aNextRunFunc; + gExpectedStatusResult = aExpectedStatusResult; + debugDump(aMsg, Components.stack.caller); + gUpdateChecker.checkForUpdates(updateCheckListener, true); +} + +function check_test_helper_pt1_1() { + Assert.equal(gUpdateCount, 1, + "the update count" + MSG_SHOULD_EQUAL); + gCheckFunc = check_test_helper_pt1_2; + let bestUpdate = gAUS.selectUpdate(gUpdates, gUpdateCount); + let state = gAUS.downloadUpdate(bestUpdate, false); + if (state == STATE_NONE || state == STATE_FAILED) { + do_throw("nsIApplicationUpdateService:downloadUpdate returned " + state); + } + gAUS.addDownloadListener(downloadListener); +} + +function check_test_helper_pt1_2() { + Assert.equal(gStatusResult, gExpectedStatusResult, + "the download status result" + MSG_SHOULD_EQUAL); + gAUS.removeDownloadListener(downloadListener); + gNextRunFunc(); +} + +function setResponseBody(aHashFunction, aHashValue, aSize) { + let patches = getRemotePatchString(null, null, + aHashFunction, aHashValue, aSize); + let updates = getRemoteUpdateString(patches); + gResponseBody = getRemoteUpdatesXMLString(updates); +} + +function initMockIncrementalDownload() { + let incrementalDownloadCID = + MockRegistrar.register(INC_CONTRACT_ID, IncrementalDownload); + do_register_cleanup(() => { + MockRegistrar.unregister(incrementalDownloadCID); + }); +} + +/* This Mock incremental downloader is used to verify that connection + * interrupts work correctly in updater code. The implementation of + * the mock incremental downloader is very simple, it simply copies + * the file to the destination location. + */ + +function IncrementalDownload() { + this.wrappedJSObject = this; +} + +IncrementalDownload.prototype = { + /* nsIIncrementalDownload */ + init: function(uri, file, chunkSize, intervalInSeconds) { + this._destination = file; + this._URI = uri; + this._finalURI = uri; + }, + + start: function(observer, ctxt) { + let tm = Cc["@mozilla.org/thread-manager;1"]. + getService(Ci.nsIThreadManager); + // Do the actual operation async to give a chance for observers + // to add themselves. + tm.mainThread.dispatch(function() { + this._observer = observer.QueryInterface(Ci.nsIRequestObserver); + this._ctxt = ctxt; + this._observer.onStartRequest(this, this._ctxt); + let mar = getTestDirFile(FILE_SIMPLE_MAR); + mar.copyTo(this._destination.parent, this._destination.leafName); + let status = Cr.NS_OK; + switch (gIncrementalDownloadErrorType++) { + case 0: + status = Cr.NS_ERROR_NET_RESET; + break; + case 1: + status = Cr.NS_ERROR_CONNECTION_REFUSED; + break; + case 2: + status = Cr.NS_ERROR_NET_RESET; + break; + case 3: + status = Cr.NS_OK; + break; + case 4: + status = Cr.NS_ERROR_OFFLINE; + // After we report offline, we want to eventually show offline + // status being changed to online. + let tm2 = Cc["@mozilla.org/thread-manager;1"]. + getService(Ci.nsIThreadManager); + tm2.mainThread.dispatch(function() { + Services.obs.notifyObservers(gAUS, + "network:offline-status-changed", + "online"); + }, Ci.nsIThread.DISPATCH_NORMAL); + break; + } + this._observer.onStopRequest(this, this._ctxt, status); + }.bind(this), Ci.nsIThread.DISPATCH_NORMAL); + }, + + get URI() { + return this._URI; + }, + + get currentSize() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + get destination() { + return this._destination; + }, + + get finalURI() { + return this._finalURI; + }, + + get totalSize() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + /* nsIRequest */ + cancel: function(aStatus) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + suspend: function() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + isPending: function() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + _loadFlags: 0, + get loadFlags() { + return this._loadFlags; + }, + set loadFlags(val) { + this._loadFlags = val; + }, + + _loadGroup: null, + get loadGroup() { + return this._loadGroup; + }, + set loadGroup(val) { + this._loadGroup = val; + }, + + _name: "", + get name() { + return this._name; + }, + + _status: 0, + get status() { + return this._status; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIIncrementalDownload]) +}; + +// Test disconnecting during an update +function run_test_pt1() { + initMockIncrementalDownload(); + setResponseBody("MD5", MD5_HASH_SIMPLE_MAR); + run_test_helper_pt1("mar download with connection interruption", + Cr.NS_OK, run_test_pt2); +} + +// Test disconnecting during an update +function run_test_pt2() { + gIncrementalDownloadErrorType = 0; + Services.prefs.setIntPref(PREF_APP_UPDATE_SOCKET_MAXERRORS, 2); + Services.prefs.setIntPref(PREF_APP_UPDATE_RETRYTIMEOUT, 0); + setResponseBody("MD5", MD5_HASH_SIMPLE_MAR); + run_test_helper_pt1("mar download with connection interruption without recovery", + Cr.NS_ERROR_NET_RESET, run_test_pt3); +} + +// Test entering offline mode while downloading +function run_test_pt3() { + gIncrementalDownloadErrorType = 4; + setResponseBody("MD5", MD5_HASH_SIMPLE_MAR); + run_test_helper_pt1("mar download with offline mode", + Cr.NS_OK, finish_test); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js new file mode 100644 index 000000000..ca065f573 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js @@ -0,0 +1,37 @@ +/* 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/. + */ + +function run_test() { + setupTestCommon(); + + debugDump("testing resuming an update download in progress for the same " + + "version of the application on startup (Bug 485624)"); + + let patches = getLocalPatchString(null, null, null, null, null, null, + STATE_DOWNLOADING); + let updates = getLocalUpdateString(patches, null, null, "1.0", "1.0"); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_DOWNLOADING); + + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + + standardInit(); + + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.activeUpdate.state, STATE_DOWNLOADING, + "the update manager activeUpdate state attribute" + + MSG_SHOULD_EQUAL); + + // Pause the download and reload the Update Manager with an empty update so + // the Application Update Service doesn't write the update xml files during + // xpcom-shutdown which will leave behind the test directory. + gAUS.pauseDownload(); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), true); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + reloadUpdateManagerData(); + + do_execute_soon(doTestFinish); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/head_update.js b/toolkit/mozapps/update/tests/unit_aus_update/head_update.js new file mode 100644 index 000000000..9715c5828 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/head_update.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const IS_SERVICE_TEST = false; + +/* import-globals-from ../data/xpcshellUtilsAUS.js */ +load("../data/xpcshellUtilsAUS.js"); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js b/toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js new file mode 100644 index 000000000..831c87257 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js @@ -0,0 +1,285 @@ +/* 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/. + */ + +var gNextRunFunc; +var gExpectedCount; + +function run_test() { + setupTestCommon(); + + debugDump("testing remote update xml attributes"); + + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + setUpdateChannel("test_channel"); + + // This test expects that the app.update.download.backgroundInterval + // preference doesn't already exist. + Services.prefs.deleteBranch("app.update.download.backgroundInterval"); + + standardInit(); + do_execute_soon(run_test_pt01); +} + +// Helper function for testing update counts returned from an update xml +function run_test_helper_pt1(aMsg, aExpectedCount, aNextRunFunc) { + gUpdates = null; + gUpdateCount = null; + gCheckFunc = check_test_helper_pt1; + gNextRunFunc = aNextRunFunc; + gExpectedCount = aExpectedCount; + debugDump(aMsg, Components.stack.caller); + gUpdateChecker.checkForUpdates(updateCheckListener, true); +} + +function check_test_helper_pt1() { + Assert.equal(gUpdateCount, gExpectedCount, + "the update count" + MSG_SHOULD_EQUAL); + gNextRunFunc(); +} + +// update xml not found +function run_test_pt01() { + run_test_helper_pt1("testing update xml not available", + null, run_test_pt02); +} + +// one update available and the update's property values +function run_test_pt02() { + debugDump("testing one update available and the update's property values"); + gUpdates = null; + gUpdateCount = null; + gCheckFunc = check_test_pt02; + let patches = getRemotePatchString("complete", "http://complete/", "SHA1", + "98db9dad8e1d80eda7e1170d0187d6f53e477059", + "9856459"); + patches += getRemotePatchString("partial", "http://partial/", "SHA1", + "e6678ca40ae7582316acdeddf3c133c9c8577de4", + "1316138"); + let updates = getRemoteUpdateString(patches, "minor", "Minor Test", + "version 2.1a1pre", "2.1a1pre", + "20080811053724", + "http://details/", + "true", + "true", "345600", "1200", + "custom1_attr=\"custom1 value\"", + "custom2_attr=\"custom2 value\""); + gResponseBody = getRemoteUpdatesXMLString(updates); + gUpdateChecker.checkForUpdates(updateCheckListener, true); +} + +function check_test_pt02() { + // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244 + // and until this is fixed this will not test the value for detailsURL when it + // isn't specified in the update xml. +// let defaultDetailsURL; +// try { + // Try using a default details URL supplied by the distribution + // if the update XML does not supply one. +// let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"]. +// getService(Ci.nsIURLFormatter); +// defaultDetailsURL = formatter.formatURLPref(PREF_APP_UPDATE_URL_DETAILS); +// } catch (e) { +// defaultDetailsURL = ""; +// } + + Assert.equal(gUpdateCount, 1, + "the update count" + MSG_SHOULD_EQUAL); + let bestUpdate = gAUS.selectUpdate(gUpdates, gUpdateCount).QueryInterface(Ci.nsIPropertyBag); + Assert.equal(bestUpdate.type, "minor", + "the update type attribute" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.name, "Minor Test", + "the update name attribute" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.displayVersion, "version 2.1a1pre", + "the update displayVersion attribute" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.appVersion, "2.1a1pre", + "the update appVersion attribute" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.buildID, "20080811053724", + "the update buildID attribute" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.detailsURL, "http://details/", + "the update detailsURL attribute" + MSG_SHOULD_EQUAL); + Assert.ok(bestUpdate.showPrompt, + "the update showPrompt attribute" + MSG_SHOULD_EQUAL); + Assert.ok(bestUpdate.showNeverForVersion, + "the update showNeverForVersion attribute" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.promptWaitTime, "345600", + "the update promptWaitTime attribute" + MSG_SHOULD_EQUAL); + // The default and maximum value for backgroundInterval is 600 + Assert.equal(bestUpdate.getProperty("backgroundInterval"), "600", + "the update backgroundInterval attribute" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.serviceURL, gURLData + gHTTPHandlerPath + "?force=1", + "the update serviceURL attribute" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.channel, "test_channel", + "the update channel attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!bestUpdate.isCompleteUpdate, + "the update isCompleteUpdate attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!bestUpdate.isSecurityUpdate, + "the update isSecurityUpdate attribute" + MSG_SHOULD_EQUAL); + // Check that installDate is within 10 seconds of the current date. + Assert.ok((Date.now() - bestUpdate.installDate) < 10000, + "the update installDate attribute should be within 10 seconds of " + + "the current time"); + Assert.ok(!bestUpdate.statusText, + "the update statusText attribute" + MSG_SHOULD_EQUAL); + // nsIUpdate:state returns an empty string when no action has been performed + // on an available update + Assert.equal(bestUpdate.state, "", + "the update state attribute" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.errorCode, 0, + "the update errorCode attribute" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.patchCount, 2, + "the update patchCount attribute" + MSG_SHOULD_EQUAL); + // XXX TODO - test nsIUpdate:serialize + + Assert.equal(bestUpdate.getProperty("custom1_attr"), "custom1 value", + "the update custom1_attr property" + MSG_SHOULD_EQUAL); + Assert.equal(bestUpdate.getProperty("custom2_attr"), "custom2 value", + "the update custom2_attr property" + MSG_SHOULD_EQUAL); + + let patch = bestUpdate.getPatchAt(0); + Assert.equal(patch.type, "complete", + "the update patch type attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.URL, "http://complete/", + "the update patch URL attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.hashFunction, "SHA1", + "the update patch hashFunction attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.hashValue, "98db9dad8e1d80eda7e1170d0187d6f53e477059", + "the update patch hashValue attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.size, 9856459, + "the update patch size attribute" + MSG_SHOULD_EQUAL); + // The value for patch.state can be the string 'null' as a valid value. This + // is confusing if it returns null which is an invalid value since the test + // failure output will show a failure for null == null. To lessen the + // confusion first check that the typeof for patch.state is string. + Assert.equal(typeof patch.state, "string", + "the update patch state typeof value should equal |string|"); + Assert.equal(patch.state, STATE_NONE, + "the update patch state attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!patch.selected, + "the update patch selected attribute" + MSG_SHOULD_EQUAL); + // XXX TODO - test nsIUpdatePatch:serialize + + patch = bestUpdate.getPatchAt(1); + Assert.equal(patch.type, "partial", + "the update patch type attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.URL, "http://partial/", + "the update patch URL attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.hashFunction, "SHA1", + "the update patch hashFunction attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.hashValue, "e6678ca40ae7582316acdeddf3c133c9c8577de4", + "the update patch hashValue attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.size, 1316138, + "the update patch size attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.state, STATE_NONE, + "the update patch state attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!patch.selected, + "the update patch selected attribute" + MSG_SHOULD_EQUAL); + // XXX TODO - test nsIUpdatePatch:serialize + + run_test_pt03(); +} + +// Empty update xml (an empty xml file returns a root node name of parsererror) +function run_test_pt03() { + gResponseBody = "<parsererror/>"; + run_test_helper_pt1("testing empty update xml", + null, run_test_pt04); +} + +// no updates available +function run_test_pt04() { + gResponseBody = getRemoteUpdatesXMLString(""); + run_test_helper_pt1("testing no updates available", + 0, run_test_pt05); +} + +// one update available with two patches +function run_test_pt05() { + let patches = getRemotePatchString("complete"); + patches += getRemotePatchString("partial"); + let updates = getRemoteUpdateString(patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + run_test_helper_pt1("testing one update available", + 1, run_test_pt06); +} + +// three updates available each with two patches +function run_test_pt06() { + let patches = getRemotePatchString("complete"); + patches += getRemotePatchString("partial"); + let updates = getRemoteUpdateString(patches); + updates += getRemoteUpdateString(patches); + updates += getRemoteUpdateString(patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + run_test_helper_pt1("testing three updates available", + 3, run_test_pt07); +} + +// one update with complete and partial patches with size 0 specified in the +// update xml +function run_test_pt07() { + let patches = getRemotePatchString("complete", null, null, null, "0"); + patches += getRemotePatchString("partial", null, null, null, "0"); + let updates = getRemoteUpdateString(patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + run_test_helper_pt1("testing one update with complete and partial " + + "patches with size 0", 0, run_test_pt08); +} + +// one update with complete patch with size 0 specified in the update xml +function run_test_pt08() { + let patches = getRemotePatchString("complete", null, null, null, "0"); + let updates = getRemoteUpdateString(patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + run_test_helper_pt1("testing one update with complete patch with size 0", + 0, run_test_pt9); +} + +// one update with partial patch with size 0 specified in the update xml +function run_test_pt9() { + let patches = getRemotePatchString("partial", null, null, null, "0"); + let updates = getRemoteUpdateString(patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + run_test_helper_pt1("testing one update with partial patch with size 0", + 0, run_test_pt10); +} + +// check that updates for older versions of the application aren't selected +function run_test_pt10() { + let patches = getRemotePatchString("complete"); + patches += getRemotePatchString("partial"); + let updates = getRemoteUpdateString(patches, "minor", null, null, "1.0pre"); + updates += getRemoteUpdateString(patches, "minor", null, null, "1.0a"); + gResponseBody = getRemoteUpdatesXMLString(updates); + run_test_helper_pt1("testing two updates older than the current version", + 2, check_test_pt10); +} + +function check_test_pt10() { + let bestUpdate = gAUS.selectUpdate(gUpdates, gUpdateCount); + Assert.ok(!bestUpdate, + "there should be no update available"); + run_test_pt11(); +} + +// check that updates for the current version of the application are selected +function run_test_pt11() { + let patches = getRemotePatchString("complete"); + patches += getRemotePatchString("partial"); + let updates = getRemoteUpdateString(patches, "minor", null, "version 1.0"); + gResponseBody = getRemoteUpdatesXMLString(updates); + run_test_helper_pt1("testing one update equal to the current version", + 1, check_test_pt11); +} + +function check_test_pt11() { + let bestUpdate = gAUS.selectUpdate(gUpdates, gUpdateCount); + Assert.ok(!!bestUpdate, + "there should be one update available"); + Assert.equal(bestUpdate.displayVersion, "version 1.0", + "the update displayVersion attribute" + MSG_SHOULD_EQUAL); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/uiAutoPref.js b/toolkit/mozapps/update/tests/unit_aus_update/uiAutoPref.js new file mode 100644 index 000000000..ee1c40bfd --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/uiAutoPref.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +Components.utils.import("resource://testing-common/MockRegistrar.jsm"); + +const WindowWatcher = { + openWindow: function(aParent, aUrl, aName, aFeatures, aArgs) { + gCheckFunc(); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher]) +}; + +const WindowMediator = { + getMostRecentWindow: function(aWindowType) { + do_execute_soon(check_status); + return { getInterface: XPCOMUtils.generateQI([Ci.nsIDOMWindow]) }; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowMediator]) +}; + +function run_test() { + setupTestCommon(); + // Calling do_get_profile prevents an error from being logged + do_get_profile(); + + debugDump("testing that an update download doesn't start when the " + + PREF_APP_UPDATE_AUTO + " preference is false"); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO, false); + Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false); + + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + standardInit(); + + let windowWatcherCID = + MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", + WindowWatcher); + let windowMediatorCID = + MockRegistrar.register("@mozilla.org/appshell/window-mediator;1", + WindowMediator); + do_register_cleanup(() => { + MockRegistrar.unregister(windowWatcherCID); + MockRegistrar.unregister(windowMediatorCID); + }); + + gCheckFunc = check_showUpdateAvailable; + let patches = getRemotePatchString("complete"); + let updates = getRemoteUpdateString(patches, "minor", null, null, "1.0"); + gResponseBody = getRemoteUpdatesXMLString(updates); + gAUS.notify(null); +} + +function check_status() { + let status = readStatusFile(); + Assert.notEqual(status, STATE_DOWNLOADING, + "the update state" + MSG_SHOULD_EQUAL); + + // Pause the download and reload the Update Manager with an empty update so + // the Application Update Service doesn't write the update xml files during + // xpcom-shutdown which will leave behind the test directory. + gAUS.pauseDownload(); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), true); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + reloadUpdateManagerData(); + + do_execute_soon(doTestFinish); +} + +function check_showUpdateAvailable() { + do_throw("showUpdateAvailable should not have called openWindow!"); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/uiSilentPref.js b/toolkit/mozapps/update/tests/unit_aus_update/uiSilentPref.js new file mode 100644 index 000000000..25110be8c --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/uiSilentPref.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +Components.utils.import("resource://testing-common/MockRegistrar.jsm"); + +/** + * Test that nsIUpdatePrompt doesn't display UI for showUpdateAvailable and + * showUpdateError when the app.update.silent preference is true. + */ + +const WindowWatcher = { + openWindow: function(aParent, aUrl, aName, aFeatures, aArgs) { + gCheckFunc(); + }, + + getNewPrompter: function(aParent) { + gCheckFunc(); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher]) +}; + +function run_test() { + setupTestCommon(); + + debugDump("testing nsIUpdatePrompt notifications should not be seen " + + "when the " + PREF_APP_UPDATE_SILENT + " preference is true"); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, true); + + let windowWatcherCID = + MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", + WindowWatcher); + do_register_cleanup(() => { + MockRegistrar.unregister(windowWatcherCID); + }); + + standardInit(); + + debugDump("testing showUpdateAvailable should not call openWindow"); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false); + let patches = getLocalPatchString(null, null, null, null, null, null, + STATE_FAILED); + let updates = getLocalUpdateString(patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_FAILED); + reloadUpdateManagerData(); + + gCheckFunc = check_showUpdateAvailable; + let update = gUpdateManager.activeUpdate; + gUP.showUpdateAvailable(update); + // Report a successful check after the call to showUpdateAvailable since it + // didn't throw and otherwise it would report no tests run. + Assert.ok(true, + "calling showUpdateAvailable should not attempt to open a window"); + + debugDump("testing showUpdateError should not call getNewPrompter"); + gCheckFunc = check_showUpdateError; + update.errorCode = WRITE_ERROR; + gUP.showUpdateError(update); + // Report a successful check after the call to showUpdateError since it + // didn't throw and otherwise it would report no tests run. + Assert.ok(true, + "calling showUpdateError should not attempt to open a window"); + + doTestFinish(); +} + +function check_showUpdateAvailable() { + do_throw("showUpdateAvailable should not have called openWindow!"); +} + +function check_showUpdateError() { + do_throw("showUpdateError should not have seen getNewPrompter!"); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/uiUnsupportedAlreadyNotified.js b/toolkit/mozapps/update/tests/unit_aus_update/uiUnsupportedAlreadyNotified.js new file mode 100644 index 000000000..5b694ed30 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/uiUnsupportedAlreadyNotified.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +Cu.import("resource://testing-common/MockRegistrar.jsm"); + +const WindowWatcher = { + openWindow: function(aParent, aUrl, aName, aFeatures, aArgs) { + check_showUpdateAvailable(); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher]) +}; + +const WindowMediator = { + getMostRecentWindow: function(aWindowType) { + return null; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowMediator]) +}; + +function run_test() { + setupTestCommon(); + + debugDump("testing nsIUpdatePrompt notifications should not be displayed " + + "when showUpdateAvailable is called for an unsupported system " + + "update when the unsupported notification has already been " + + "shown (bug 843497)"); + + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + standardInit(); + + let windowWatcherCID = + MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", + WindowWatcher); + let windowMediatorCID = + MockRegistrar.register("@mozilla.org/appshell/window-mediator;1", + WindowMediator); + do_register_cleanup(() => { + MockRegistrar.unregister(windowWatcherCID); + MockRegistrar.unregister(windowMediatorCID); + }); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false); + Services.prefs.setBoolPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, true); + // This preference is used to determine when the background update check has + // completed since a successful check will clear the preference. + Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, 1); + + gResponseBody = getRemoteUpdatesXMLString(" <update type=\"major\" " + + "name=\"Unsupported Update\" " + + "unsupported=\"true\" " + + "detailsURL=\"" + URL_HOST + + "\"></update>\n"); + gAUS.notify(null); + do_execute_soon(check_test); +} + +function check_test() { + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) { + do_execute_soon(check_test); + return; + } + Assert.ok(true, + PREF_APP_UPDATE_BACKGROUNDERRORS + " preference should not exist"); + + stop_httpserver(doTestFinish); +} + +function check_showUpdateAvailable() { + do_throw("showUpdateAvailable should not have called openWindow!"); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js b/toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js new file mode 100644 index 000000000..e46469455 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js @@ -0,0 +1,177 @@ +/* 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/. + */ + +function run_test() { + setupTestCommon(); + + debugDump("testing addition of a successful update to " + FILE_UPDATES_XML + + " and verification of update properties including the format " + + "prior to bug 530872"); + + setUpdateChannel("test_channel"); + + // This test expects that the app.update.download.backgroundInterval + // preference doesn't already exist. + Services.prefs.deleteBranch("app.update.download.backgroundInterval"); + + // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244 + // and until bug 470244 is fixed this will not test the value for detailsURL + // when it isn't specified in the update xml. + let patches = getLocalPatchString("partial", "http://partial/", "SHA256", + "cd43", "86", "true", STATE_PENDING); + let updates = getLocalUpdateString(patches, "major", "New", "version 4", + "4.0", "20070811053724", + "http://details1/", + "http://service1/", "1238441300314", + "test status text", "false", + "test_channel", "true", "true", "true", + "345600", "300", "3.0", + "custom1_attr=\"custom1 value\"", + "custom2_attr=\"custom2 value\""); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_SUCCEEDED); + + patches = getLocalPatchString("complete", "http://complete/", "SHA1", "6232", + "75", "true", STATE_FAILED); + updates = getLocalUpdateString(patches, "major", "Existing", null, "3.0", + null, + "http://details2/", + "http://service2/", null, + getString("patchApplyFailure"), "true", + "test_channel", "false", null, null, "691200", + null, null, + "custom3_attr=\"custom3 value\"", + "custom4_attr=\"custom4 value\""); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), false); + + standardInit(); + + Assert.ok(!gUpdateManager.activeUpdate, + "the update manager activeUpdate attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 2, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + + debugDump("checking the activeUpdate properties"); + let update = gUpdateManager.getUpdateAt(0).QueryInterface(Ci.nsIPropertyBag); + Assert.equal(update.state, STATE_SUCCEEDED, + "the update state attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.type, "major", + "the update type attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.name, "New", + "the update name attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.displayVersion, "version 4", + "the update displayVersion attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.appVersion, "4.0", + "the update appVersion attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.buildID, "20070811053724", + "the update buildID attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.detailsURL, "http://details1/", + "the update detailsURL attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.serviceURL, "http://service1/", + "the update serviceURL attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.installDate, "1238441300314", + "the update installDate attribute" + MSG_SHOULD_EQUAL); + // statusText is updated + Assert.equal(update.statusText, getString("installSuccess"), + "the update statusText attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!update.isCompleteUpdate, + "the update isCompleteUpdate attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.channel, "test_channel", + "the update channel attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!!update.showPrompt, + "the update showPrompt attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!!update.showNeverForVersion, + "the update showNeverForVersion attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.promptWaitTime, "345600", + "the update promptWaitTime attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.getProperty("backgroundInterval"), "300", + "the update backgroundInterval attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.previousAppVersion, "3.0", + "the update previousAppVersion attribute" + MSG_SHOULD_EQUAL); + // Custom attributes + Assert.equal(update.getProperty("custom1_attr"), "custom1 value", + "the update custom1_attr property" + MSG_SHOULD_EQUAL); + Assert.equal(update.getProperty("custom2_attr"), "custom2 value", + "the update custom2_attr property" + MSG_SHOULD_EQUAL); + + debugDump("checking the activeUpdate patch properties"); + let patch = update.selectedPatch; + Assert.equal(patch.type, "partial", + "the update patch type attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.URL, "http://partial/", + "the update patch URL attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.hashFunction, "SHA256", + "the update patch hashFunction attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.hashValue, "cd43", + "the update patch hashValue attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.size, "86", + "the update patch size attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!!patch.selected, + "the update patch selected attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.state, STATE_SUCCEEDED, + "the update patch state attribute" + MSG_SHOULD_EQUAL); + + debugDump("checking the first update properties"); + update = gUpdateManager.getUpdateAt(1).QueryInterface(Ci.nsIPropertyBag); + Assert.equal(update.state, STATE_FAILED, + "the update state attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.name, "Existing", + "the update name attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.type, "major", + "the update type attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.displayVersion, "3.0", + "the update displayVersion attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.appVersion, "3.0", + "the update appVersion attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.detailsURL, "http://details2/", + "the update detailsURL attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.serviceURL, "http://service2/", + "the update serviceURL attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.installDate, "1238441400314", + "the update installDate attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.statusText, getString("patchApplyFailure"), + "the update statusText attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.buildID, "20080811053724", + "the update buildID attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!!update.isCompleteUpdate, + "the update isCompleteUpdate attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.channel, "test_channel", + "the update channel attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!update.showPrompt, + "the update showPrompt attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!update.showNeverForVersion, + "the update showNeverForVersion attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.promptWaitTime, "691200", + "the update promptWaitTime attribute" + MSG_SHOULD_EQUAL); + // The default and maximum value for backgroundInterval is 600 + Assert.equal(update.getProperty("backgroundInterval"), "600", + "the update backgroundInterval attribute" + MSG_SHOULD_EQUAL); + Assert.equal(update.previousAppVersion, null, + "the update previousAppVersion attribute" + MSG_SHOULD_EQUAL); + // Custom attributes + Assert.equal(update.getProperty("custom3_attr"), "custom3 value", + "the update custom3_attr property" + MSG_SHOULD_EQUAL); + Assert.equal(update.getProperty("custom4_attr"), "custom4 value", + "the update custom4_attr property" + MSG_SHOULD_EQUAL); + + debugDump("checking the first update patch properties"); + patch = update.selectedPatch; + Assert.equal(patch.type, "complete", + "the update patch type attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.URL, "http://complete/", + "the update patch URL attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.hashFunction, "SHA1", + "the update patch hashFunction attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.hashValue, "6232", + "the update patch hashValue attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.size, "75", + "the update patch size attribute" + MSG_SHOULD_EQUAL); + Assert.ok(!!patch.selected, + "the update patch selected attribute" + MSG_SHOULD_EQUAL); + Assert.equal(patch.state, STATE_FAILED, + "the update patch state attribute" + MSG_SHOULD_EQUAL); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js b/toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js new file mode 100644 index 000000000..db7d90094 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js @@ -0,0 +1,305 @@ +/* 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/. + */ + +/* General URL Construction Tests */ + +const URL_PREFIX = URL_HOST + "/"; + +var gAppInfo; + +// Since gUpdateChecker.checkForUpdates uses XMLHttpRequest and XMLHttpRequest +// can be slow it combines the checks whenever possible. +function run_test() { + // This test needs access to omni.ja to read the update.locale file so don't + // use a custom directory for the application directory. + gUseTestAppDir = false; + setupTestCommon(); + + standardInit(); + gAppInfo = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULAppInfo). + QueryInterface(Ci.nsIXULRuntime); + do_execute_soon(run_test_pt1); +} + + +// url constructed with: +// %PRODUCT% +// %VERSION% +// %BUILD_ID% +// %BUILD_TARGET% +// %LOCALE% +// %CHANNEL% +// %PLATFORM_VERSION% +// %OS_VERSION% +// %SYSTEM_CAPABILITIES% +// %DISTRIBUTION% +// %DISTRIBUTION_VERSION% +function run_test_pt1() { + gCheckFunc = check_test_pt1; + // The code that gets the locale accesses the profile which is only available + // after calling do_get_profile in xpcshell tests. This prevents an error from + // being logged. + do_get_profile(); + + setUpdateChannel("test_channel"); + gDefaultPrefBranch.setCharPref(PREF_DISTRIBUTION_ID, "test_distro"); + gDefaultPrefBranch.setCharPref(PREF_DISTRIBUTION_VERSION, "test_distro_version"); + + let url = URL_PREFIX + "%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/" + + "%LOCALE%/%CHANNEL%/%PLATFORM_VERSION%/%OS_VERSION%/" + + "%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/" + + "updates.xml"; + debugDump("testing url construction - url: " + url); + setUpdateURL(url); + try { + gUpdateChecker.checkForUpdates(updateCheckListener, true); + } catch (e) { + debugDump("The following error is most likely due to a missing " + + "update.locale file"); + do_throw(e); + } +} + +function check_test_pt1() { + let url = URL_PREFIX + gAppInfo.name + "/" + gAppInfo.version + "/" + + gAppInfo.appBuildID + "/" + gAppInfo.OS + "_" + getABI() + "/" + + INSTALL_LOCALE + "/test_channel/" + gAppInfo.platformVersion + "/" + + getOSVersion() + "/" + getSystemCapabilities() + + "/test_distro/test_distro_version/updates.xml?force=1"; + // Log the urls since Assert.equal won't print the entire urls to the log. + if (gRequestURL != url) { + logTestInfo("expected url: " + url); + logTestInfo("returned url: " + gRequestURL); + } + Assert.equal(gRequestURL, url, + "the url" + MSG_SHOULD_EQUAL); + run_test_pt2(); +} + +// url constructed with: +// %CHANNEL% with distribution partners +// %CUSTOM% parameter +// force param when there already is a param - bug 454357 +function run_test_pt2() { + gCheckFunc = check_test_pt2; + let url = URL_PREFIX + "%CHANNEL%/updates.xml?custom=%CUSTOM%"; + debugDump("testing url constructed with %CHANNEL% - " + url); + setUpdateURL(url); + gDefaultPrefBranch.setCharPref(PREFBRANCH_APP_PARTNER + "test_partner1", + "test_partner1"); + gDefaultPrefBranch.setCharPref(PREFBRANCH_APP_PARTNER + "test_partner2", + "test_partner2"); + Services.prefs.setCharPref("app.update.custom", "custom"); + gUpdateChecker.checkForUpdates(updateCheckListener, true); +} + +function check_test_pt2() { + let url = URL_PREFIX + "test_channel-cck-test_partner1-test_partner2/" + + "updates.xml?custom=custom&force=1"; + Assert.equal(gRequestURL, url, + "the url" + MSG_SHOULD_EQUAL); + doTestFinish(); +} + +function getABI() { + let abi; + try { + abi = gAppInfo.XPCOMABI; + } catch (e) { + do_throw("nsIXULAppInfo:XPCOMABI not defined\n"); + } + + if (IS_MACOSX) { + // Mac universal build should report a different ABI than either macppc + // or mactel. This is necessary since nsUpdateService.js will set the ABI to + // Universal-gcc3 for Mac universal builds. + let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"]. + getService(Ci.nsIMacUtils); + + if (macutils.isUniversalBinary) { + abi += "-u-" + macutils.architecturesInBinary; + } + } else if (IS_WIN) { + // Windows build should report the CPU architecture that it's running on. + abi += "-" + getProcArchitecture(); + } + return abi; +} + +function getOSVersion() { + let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); + let osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version"); + + if (IS_WIN) { + try { + let servicePack = getServicePack(); + osVersion += "." + servicePack; + } catch (e) { + do_throw("Failure obtaining service pack: " + e); + } + + if ("5.0" === sysInfo.getProperty("version")) { // Win2K + osVersion += " (unknown)"; + } else { + try { + osVersion += " (" + getProcArchitecture() + ")"; + } catch (e) { + do_throw("Failed to obtain processor architecture: " + e); + } + } + } + + if (osVersion) { + try { + osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")"; + } catch (e) { + // Not all platforms have a secondary widget library, so an error is + // nothing to worry about. + } + osVersion = encodeURIComponent(osVersion); + } + return osVersion; +} + +function getServicePack() { + // NOTE: This function is a helper function and not a test. Thus, + // it uses throw() instead of do_throw(). Any tests that use this function + // should catch exceptions thrown in this function and deal with them + // appropriately (usually by calling do_throw). + const BYTE = ctypes.uint8_t; + const WORD = ctypes.uint16_t; + const DWORD = ctypes.uint32_t; + const WCHAR = ctypes.char16_t; + const BOOL = ctypes.int; + + // This structure is described at: + // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx + const SZCSDVERSIONLENGTH = 128; + const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', + [ + {dwOSVersionInfoSize: DWORD}, + {dwMajorVersion: DWORD}, + {dwMinorVersion: DWORD}, + {dwBuildNumber: DWORD}, + {dwPlatformId: DWORD}, + {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)}, + {wServicePackMajor: WORD}, + {wServicePackMinor: WORD}, + {wSuiteMask: WORD}, + {wProductType: BYTE}, + {wReserved: BYTE} + ]); + + let kernel32 = ctypes.open("kernel32"); + try { + let GetVersionEx = kernel32.declare("GetVersionExW", + ctypes.default_abi, + BOOL, + OSVERSIONINFOEXW.ptr); + let winVer = OSVERSIONINFOEXW(); + winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size; + + if (0 === GetVersionEx(winVer.address())) { + // Using "throw" instead of "do_throw" (see NOTE above) + throw ("Failure in GetVersionEx (returned 0)"); + } + + return winVer.wServicePackMajor + "." + winVer.wServicePackMinor; + } finally { + kernel32.close(); + } +} + +function getProcArchitecture() { + // NOTE: This function is a helper function and not a test. Thus, + // it uses throw() instead of do_throw(). Any tests that use this function + // should catch exceptions thrown in this function and deal with them + // appropriately (usually by calling do_throw). + const WORD = ctypes.uint16_t; + const DWORD = ctypes.uint32_t; + + // This structure is described at: + // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx + const SYSTEM_INFO = new ctypes.StructType('SYSTEM_INFO', + [ + {wProcessorArchitecture: WORD}, + {wReserved: WORD}, + {dwPageSize: DWORD}, + {lpMinimumApplicationAddress: ctypes.voidptr_t}, + {lpMaximumApplicationAddress: ctypes.voidptr_t}, + {dwActiveProcessorMask: DWORD.ptr}, + {dwNumberOfProcessors: DWORD}, + {dwProcessorType: DWORD}, + {dwAllocationGranularity: DWORD}, + {wProcessorLevel: WORD}, + {wProcessorRevision: WORD} + ]); + + let kernel32 = ctypes.open("kernel32"); + try { + let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo", + ctypes.default_abi, + ctypes.void_t, + SYSTEM_INFO.ptr); + let sysInfo = SYSTEM_INFO(); + // Default to unknown + sysInfo.wProcessorArchitecture = 0xffff; + + GetNativeSystemInfo(sysInfo.address()); + switch (sysInfo.wProcessorArchitecture) { + case 9: + return "x64"; + case 6: + return "IA64"; + case 0: + return "x86"; + default: + // Using "throw" instead of "do_throw" (see NOTE above) + throw ("Unknown architecture returned from GetNativeSystemInfo: " + sysInfo.wProcessorArchitecture); + } + } finally { + kernel32.close(); + } +} + +/** + * Provides system capability information for application update though it may + * be used by other consumers. + */ +function getSystemCapabilities() { + if (IS_WIN) { + const PF_MMX_INSTRUCTIONS_AVAILABLE = 3; // MMX + const PF_XMMI_INSTRUCTIONS_AVAILABLE = 6; // SSE + const PF_XMMI64_INSTRUCTIONS_AVAILABLE = 10; // SSE2 + const PF_SSE3_INSTRUCTIONS_AVAILABLE = 13; // SSE3 + + let lib = ctypes.open("kernel32.dll"); + let IsProcessorFeaturePresent = lib.declare("IsProcessorFeaturePresent", + ctypes.winapi_abi, + ctypes.int32_t, /* success */ + ctypes.uint32_t); /* DWORD */ + let instructionSet = "unknown"; + try { + if (IsProcessorFeaturePresent(PF_SSE3_INSTRUCTIONS_AVAILABLE)) { + instructionSet = "SSE3"; + } else if (IsProcessorFeaturePresent(PF_XMMI64_INSTRUCTIONS_AVAILABLE)) { + instructionSet = "SSE2"; + } else if (IsProcessorFeaturePresent(PF_XMMI_INSTRUCTIONS_AVAILABLE)) { + instructionSet = "SSE"; + } else if (IsProcessorFeaturePresent(PF_MMX_INSTRUCTIONS_AVAILABLE)) { + instructionSet = "MMX"; + } + } catch (e) { + Cu.reportError("Error getting processor instruction set. " + + "Exception: " + e); + } + + lib.close(); + return instructionSet; + } + + return "NA"; +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini b/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini new file mode 100644 index 000000000..0d2205046 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini @@ -0,0 +1,27 @@ +; 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/. + +[DEFAULT] +tags = appupdate +head = head_update.js +tail = + +[canCheckForAndCanApplyUpdates.js] +[urlConstruction.js] +[updateManagerXML.js] +[remoteUpdateXML.js] +[downloadAndHashCheckMar.js] +[cleanupDownloadingForOlderAppVersion.js] +[cleanupDownloadingForDifferentChannel.js] +[cleanupDownloadingForSameVersionAndBuildID.js] +[cleanupDownloadingIncorrectStatus.js] +[cleanupPendingVersionFileIncorrectStatus.js] +[cleanupSuccessLogMove.js] +[cleanupSuccessLogsFIFO.js] +[downloadInterruptedRecovery.js] +[downloadResumeForSameAppVersion.js] +[downloadCompleteAfterPartialFailure.js] +[uiSilentPref.js] +[uiUnsupportedAlreadyNotified.js] +[uiAutoPref.js] diff --git a/toolkit/mozapps/update/tests/unit_base_updater/.eslintrc.js b/toolkit/mozapps/update/tests/unit_base_updater/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/mozapps/update/tests/unit_base_updater/head_update.js b/toolkit/mozapps/update/tests/unit_base_updater/head_update.js new file mode 100644 index 000000000..9715c5828 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/head_update.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const IS_SERVICE_TEST = false; + +/* import-globals-from ../data/xpcshellUtilsAUS.js */ +load("../data/xpcshellUtilsAUS.js"); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js new file mode 100644 index 000000000..f3f767394 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js @@ -0,0 +1,37 @@ +/* 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/. + */ + +/* Callback file not in install directory or a sub-directory of the install + directory failure */ + +const STATE_AFTER_RUNUPDATE = STATE_FAILED_INVALID_CALLBACK_DIR_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = getTestDirFile(FILE_HELPER_BIN).path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, null, path); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js new file mode 100644 index 000000000..969f84f9d --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js @@ -0,0 +1,45 @@ +/* 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/. + */ + +/* Too long callback file path failure test */ + +const STATE_AFTER_RUNUPDATE = STATE_FAILED_INVALID_CALLBACK_PATH_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = "123456789"; + if (IS_WIN) { + path = "\\" + path; + path = path.repeat(30); // 300 characters + path = "C:" + path; + } else { + path = "/" + path; + path = path.repeat(1000); // 10000 characters + } + + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, null, path); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js new file mode 100644 index 000000000..70e03646a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js @@ -0,0 +1,47 @@ +/* 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/. + */ + +/* Too long install directory path failure test */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR + : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = "123456789"; + if (IS_WIN) { + path = "\\" + path; + path = path.repeat(30); // 300 characters + path = "C:" + path; + } else { + path = "/" + path; + path = path.repeat(1000); // 10000 characters + } + + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js new file mode 100644 index 000000000..330578de6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js @@ -0,0 +1,44 @@ +/* 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/. + */ + +/* Install directory path traversal failure test */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR + : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = "123456789"; + if (IS_WIN) { + path = "C:\\" + path + "\\..\\" + path; + } else { + path = "/" + path + "/../" + path; + } + + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js new file mode 100644 index 000000000..8ddb34af0 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js @@ -0,0 +1,38 @@ +/* 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/. + */ + +/* Different install and working directories for a regular update failure */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_ERROR + : STATE_FAILED_INVALID_APPLYTO_DIR_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = getApplyDirFile("..", false).path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js new file mode 100644 index 000000000..c8ae3f0c6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js @@ -0,0 +1,43 @@ +/* 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/. + */ + +/* Patch directory path traversal failure test */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = getUpdatesPatchDir(); + if (IS_WIN) { + path = path + "\\..\\"; + } else { + path = path + "/../"; + } + + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, path, null, null, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js new file mode 100644 index 000000000..e9b227657 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js @@ -0,0 +1,38 @@ +/* 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/. + */ + +/* Different install and working directories for a regular update failure */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR + : STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = getApplyDirFile("..", false).path; + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true, null, null, path, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js new file mode 100644 index 000000000..87bbad4aa --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js @@ -0,0 +1,38 @@ +/* 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/. + */ + +/* Working directory path local UNC failure test */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR + : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = "\\\\.\\" + getApplyDirFile(null, false).path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js new file mode 100644 index 000000000..a550909b2 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js @@ -0,0 +1,37 @@ +/* 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/. + */ + +/* Relative working directory path failure test */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR + : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, "test", null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js new file mode 100644 index 000000000..b9f793236 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +const STATE_AFTER_STAGE = STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + createUpdateInProgressLockFile(getAppBaseDir()); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(false); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + removeUpdateInProgressLockFile(getAppBaseDir()); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(PERFORMING_STAGED_UPDATE); + checkUpdateLogContains(ERR_UPDATE_IN_PROGRESS); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js new file mode 100644 index 000000000..a606720b7 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, undefined); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true, false); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + lockDirectory(getAppBaseDir().path); + // Switch the application to the staged application that was updated. + runUpdateUsingApp(STATE_SUCCEEDED); +} + +/** + * Called after the call to runUpdateUsingApp finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + + let updatesDir = getUpdatesPatchDir(); + Assert.ok(updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path)); + + let log = getUpdateLog(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js new file mode 100644 index 000000000..00b38adc7 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test a replace request for a staged update with a version file that specifies + * an older version failure. The same check is used in nsUpdateDriver.cpp for + * all update types which is why there aren't tests for the maintenance service + * as well as for other update types. + */ + +const STATE_AFTER_STAGE = STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(false); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Change the active update to an older version to simulate installing a new + // version of the application while there is an update that has been staged. + let channel = gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL); + let patches = getLocalPatchString(null, null, null, null, null, "true", + STATE_AFTER_STAGE); + let updates = getLocalUpdateString(patches, null, null, null, "1.0", null, + null, null, null, null, "true", channel); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + // Change the version file to an older version to simulate installing a new + // version of the application while there is an update that has been staged. + writeVersionFile("1.0"); + reloadUpdateManagerData(); + // Try to switch the application to the staged application that was updated. + runUpdateUsingApp(STATE_AFTER_STAGE); +} + +/** + * Called after the call to runUpdateUsingApp finishes. + */ +function runUpdateFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_STAGE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile, IS_MACOSX ? false : true, false); + + let updatesDir = getUpdatesPatchDir(); + Assert.ok(updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path)); + + let log = getUpdateLog(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js new file mode 100644 index 000000000..5b9b08156 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, true); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdateUsingApp(STATE_SUCCEEDED); +} + +/** + * Called after the call to runUpdateUsingApp finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + + let updatesDir = getUpdatesPatchDir(); + Assert.ok(updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path)); + + let log = getUpdateLog(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js new file mode 100644 index 000000000..e76233fe6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + // The third parameter will test that a full path to the post update binary + // doesn't execute. + setupUpdaterTest(FILE_COMPLETE_MAR, undefined, + getApplyDirFile(null, true).path + "/"); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdateUsingApp(STATE_SUCCEEDED); +} + +/** + * Called after the call to runUpdateUsingApp finishes. + */ +function runUpdateFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + + let updatesDir = getUpdatesPatchDir(); + Assert.ok(updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path)); + + let log = getUpdateLog(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js new file mode 100644 index 000000000..b1505d58e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file staged patch apply failure test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; +const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js new file mode 100644 index 000000000..a1cc7d043 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file staged patch apply success test */ + +const START_STATE = STATE_PENDING; +const STATE_AFTER_STAGE = STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, true); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + setupSymLinks(); + runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + checkSymLinks(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + checkCallbackLog(); +} + +/** + * Setup symlinks for the test. + */ +function setupSymLinks() { + if (IS_UNIX) { + removeSymlink(); + createSymlink(); + do_register_cleanup(removeSymlink); + gTestFiles.splice(gTestFiles.length - 3, 0, + { + description: "Readable symlink", + fileName: "link", + relPathDir: DIR_RESOURCES, + originalContents: "test", + compareContents: "test", + originalFile: null, + compareFile: null, + originalPerms: 0o666, + comparePerms: 0o666 + }); + } +} + +/** + * Checks the state of the symlinks for the test. + */ +function checkSymLinks() { + if (IS_UNIX) { + checkSymlink(); + } +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js new file mode 100644 index 000000000..93333cade --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js new file mode 100644 index 000000000..79e54c182 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Replace app binary complete MAR file staged patch apply success test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + gCallbackBinFile = "exe0.exe"; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js new file mode 100644 index 000000000..b1f84715f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Patch app binary partial MAR file staged patch apply success test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + gCallbackBinFile = "exe0.exe"; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js new file mode 100644 index 000000000..85e92d290 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Replace app binary complete MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + gCallbackBinFile = "exe0.exe"; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js new file mode 100644 index 000000000..1212c9ba2 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Patch app binary partial MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + gCallbackBinFile = "exe0.exe"; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js new file mode 100644 index 000000000..960c96f7b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js @@ -0,0 +1,47 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Failure Test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[11].originalFile = "partial.png"; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + // If execv is used the updater process will turn into the callback process + // and the updater's return code will be that of the callback process. + runUpdate(STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE, false, (USE_EXECV ? 0 : 1), + true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusFile(), STATE_NONE, + "the status file failure code" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, LOADSOURCE_ERROR_WRONG_SIZE, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_FAILURE); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js new file mode 100644 index 000000000..b39595f92 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file staged patch apply failure test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; +const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestFiles[13].relPathDir + gTestFiles[13].fileName, + false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js new file mode 100644 index 000000000..06d386ad6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file staged patch apply failure test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; +const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestFiles[11].relPathDir + gTestFiles[11].fileName, + false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js new file mode 100644 index 000000000..89a2fff5e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestFiles[13].relPathDir + gTestFiles[13].fileName, + false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js new file mode 100644 index 000000000..ea85ddccc --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestFiles[11].relPathDir + gTestFiles[11].fileName, + false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js new file mode 100644 index 000000000..c5efaa8c0 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file patch apply failure test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperLockFile(gTestFiles[3]); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusFile(), STATE_PENDING, + "the status file failure code" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_PENDING, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, WRITE_ERROR, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_BACKUP_CREATE_7); + checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js new file mode 100644 index 000000000..4fdbadb5b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file patch apply failure test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperLockFile(gTestFiles[2]); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_FAILED_READ_ERROR, false, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusFile(), STATE_NONE, + "the status file failure code" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, READ_ERROR, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_UNABLE_OPEN_DEST); + checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js new file mode 100644 index 000000000..4d12f4e42 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file staged patch apply failure test */ + +const STATE_AFTER_STAGE = STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperLockFile(gTestFiles[3]); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + // Files aren't checked after staging since this test locks a file which + // prevents reading the file. + checkUpdateLogContains(ERR_ENSURE_COPY); + // Switch the application to the staged application that was updated. + runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, false); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusFile(), STATE_PENDING, + "the status file failure code" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 2, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_PENDING, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, WRITE_ERROR, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_BACKUP_CREATE_7); + checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js new file mode 100644 index 000000000..5f64df34c --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file staged patch apply failure test */ + +const STATE_AFTER_STAGE = STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperLockFile(gTestFiles[2]); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + // Files aren't checked after staging since this test locks a file which + // prevents reading the file. + checkUpdateLogContains(ERR_ENSURE_COPY); + // Switch the application to the staged application that was updated. + runUpdate(STATE_FAILED_READ_ERROR, false, 1, false); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusFile(), STATE_NONE, + "the status file failure code" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 2, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, READ_ERROR, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_UNABLE_OPEN_DEST); + checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js new file mode 100644 index 000000000..b83bafccc --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file staged patch apply failure + test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; +const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestDirs[4].relPathDir + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0], true); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js new file mode 100644 index 000000000..39ea485cd --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file staged patch apply failure + test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; +const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestDirs[2].relPathDir + gTestDirs[2].files[0], true); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js new file mode 100644 index 000000000..a71bb8d49 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestDirs[4].relPathDir + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0], true); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js new file mode 100644 index 000000000..2cbe70ed8 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestDirs[2].relPathDir + gTestDirs[2].files[0], true); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js new file mode 100644 index 000000000..a9ce23420 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js @@ -0,0 +1,41 @@ +/* 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/. + */ + +/* General Partial MAR File Staged Patch Apply Failure Test */ + +const STATE_AFTER_STAGE = STATE_FAILED; +gStagingRemovedUpdate = true; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[11].originalFile = "partial.png"; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, LOADSOURCE_ERROR_WRONG_SIZE, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_LOADSOURCEFILE_FAILED); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js new file mode 100644 index 000000000..f7745f68f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js @@ -0,0 +1,132 @@ +/* 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/. + */ + +/* General Complete MAR File Staged Patch Apply Test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsCompleteSuccess; + setupDistributionDir(); + setupSymLinks(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + checkSymLinks(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + checkDistributionDir(); + checkCallbackLog(); +} + +/** + * Setup the state of the distribution directory for the test. + */ +function setupDistributionDir() { + if (IS_MACOSX) { + // Create files in the old distribution directory location to verify that + // the directory and its contents are removed when there is a distribution + // directory in the new location. + let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true); + writeFile(testFile, "test\n"); + testFile = getApplyDirFile(DIR_MACOS + "distribution/test1/testFile", true); + writeFile(testFile, "test\n"); + } +} + +/** + * Checks the state of the distribution directory for the test. + */ +function checkDistributionDir() { + if (IS_MACOSX) { + let distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + checkUpdateLogContains(REMOVE_OLD_DIST_DIR); + } +} + +/** + * Setup symlinks for the test. + */ +function setupSymLinks() { + // Don't test symlinks on Mac OS X in this test since it tends to timeout. + // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js + if (IS_UNIX && !IS_MACOSX) { + removeSymlink(); + createSymlink(); + do_register_cleanup(removeSymlink); + gTestFiles.splice(gTestFiles.length - 3, 0, + { + description: "Readable symlink", + fileName: "link", + relPathDir: DIR_RESOURCES, + originalContents: "test", + compareContents: "test", + originalFile: null, + compareFile: null, + originalPerms: 0o666, + comparePerms: 0o666 + }); + } +} + +/** + * Checks the state of the symlinks for the test. + */ +function checkSymLinks() { + // Don't test symlinks on Mac OS X in this test since it tends to timeout. + // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js + if (IS_UNIX && !IS_MACOSX) { + checkSymlink(); + } +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js new file mode 100644 index 000000000..ef15326de --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js @@ -0,0 +1,112 @@ +/* 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/. + */ + +/* General Partial MAR File Staged Patch Apply Test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[gTestFiles.length - 2].originalContents = null; + gTestFiles[gTestFiles.length - 2].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 2].comparePerms = 0o644; + gTestDirs = gTestDirsPartialSuccess; + preventDistributionFiles(); + setupDistributionDir(); + setupUpdaterTest(FILE_PARTIAL_MAR, true); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true, false, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true, true); + checkDistributionDir(); + checkCallbackLog(); +} + +/** + * Setup the state of the distribution directory for the test. + */ +function setupDistributionDir() { + if (IS_MACOSX) { + // Create files in the old distribution directory location to verify that + // the directory and its contents are moved to the new location on update. + let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true); + writeFile(testFile, "test\n"); + testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true); + writeFile(testFile, "test\n"); + } +} + +/** + * Checks the state of the distribution directory. + */ +function checkDistributionDir() { + let distributionDir = getApplyDirFile(DIR_RESOURCES + "distribution", true); + if (IS_MACOSX) { + Assert.ok(distributionDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(distributionDir.path)); + + let testFile = getApplyDirFile(DIR_RESOURCES + "distribution/testFile", true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + + testFile = getApplyDirFile(DIR_RESOURCES + "distribution/test/testFile", true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + + distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + + checkUpdateLogContains(MOVE_OLD_DIST_DIR); + } else { + debugDump("testing that files aren't added with an add-if instruction " + + "when the file's destination directory doesn't exist"); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + } +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js new file mode 100644 index 000000000..1008e867f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js @@ -0,0 +1,96 @@ +/* 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/. + */ + +/* General Complete MAR File Patch Apply Test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + preventDistributionFiles(); + setupDistributionDir(); + setupUpdaterTest(FILE_COMPLETE_MAR, true); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, false, false, true); + checkDistributionDir(); + checkCallbackLog(); +} + +/** + * Setup the state of the distribution directory for the test. + */ +function setupDistributionDir() { + if (IS_MACOSX) { + // Create files in the old distribution directory location to verify that + // the directory and its contents are moved to the new location on update. + let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true); + writeFile(testFile, "test\n"); + testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true); + writeFile(testFile, "test\n"); + } +} + +/** + * Checks the state of the distribution directory. + */ +function checkDistributionDir() { + let distributionDir = getApplyDirFile(DIR_RESOURCES + "distribution", true); + if (IS_MACOSX) { + Assert.ok(distributionDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(distributionDir.path)); + + let testFile = getApplyDirFile(DIR_RESOURCES + "distribution/testFile", true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + + testFile = getApplyDirFile(DIR_RESOURCES + "distribution/test/testFile", true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + + distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + + checkUpdateLogContains(MOVE_OLD_DIST_DIR); + } else { + debugDump("testing that files aren't added with an add-if instruction " + + "when the file's destination directory doesn't exist"); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + } +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js new file mode 100644 index 000000000..616390f55 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js @@ -0,0 +1,79 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestFiles[gTestFiles.length - 2].originalContents = null; + gTestFiles[gTestFiles.length - 2].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 2].comparePerms = 0o644; + gTestDirs = gTestDirsPartialSuccess; + setupDistributionDir(); + // The third parameter will test that a relative path that contains a + // directory traversal to the post update binary doesn't execute. + setupUpdaterTest(FILE_PARTIAL_MAR, false, "test/../"); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + checkDistributionDir(); + checkCallbackLog(); +} + +/** + * Setup the state of the distribution directory for the test. + */ +function setupDistributionDir() { + if (IS_MACOSX) { + // Create files in the old distribution directory location to verify that + // the directory and its contents are removed when there is a distribution + // directory in the new location. + let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true); + writeFile(testFile, "test\n"); + testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true); + writeFile(testFile, "test\n"); + } +} + +/** + * Checks the state of the distribution directory. + */ +function checkDistributionDir() { + if (IS_MACOSX) { + let distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + checkUpdateLogContains(REMOVE_OLD_DIST_DIR); + } +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js b/toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js new file mode 100644 index 000000000..86a2eb821 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js @@ -0,0 +1,51 @@ +/* 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/. + */ + +/* Test version downgrade MAR security check */ + +function run_test() { + if (!MOZ_VERIFY_MAR_SIGNATURE) { + return; + } + + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_OLD_VERSION_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + // If execv is used the updater process will turn into the callback process + // and the updater's return code will be that of the callback process. + runUpdate(STATE_FAILED_VERSION_DOWNGRADE_ERROR, false, (USE_EXECV ? 0 : 1), + false); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, VERSION_DOWNGRADE_ERROR, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(STATE_FAILED_VERSION_DOWNGRADE_ERROR); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js new file mode 100644 index 000000000..6db906fbc --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js @@ -0,0 +1,51 @@ +/* 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/. + */ + +/* Test product/channel MAR security check */ + +function run_test() { + if (!MOZ_VERIFY_MAR_SIGNATURE) { + return; + } + + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_WRONG_CHANNEL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + // If execv is used the updater process will turn into the callback process + // and the updater's return code will be that of the callback process. + runUpdate(STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR, false, (USE_EXECV ? 0 : 1), + false); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, MAR_CHANNEL_MISMATCH_ERROR, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini b/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini new file mode 100644 index 000000000..2b77bee7a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini @@ -0,0 +1,136 @@ +; 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/. + +; Tests that require the updater binary. These tests should never run on Android +; which doesn't use the updater binary as other applications do and are excluded +; from running the tests in the moz.build file. + +[DEFAULT] +tags = appupdate +head = head_update.js +tail = + +[invalidArgCallbackFileNotInInstallDirFailure.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[invalidArgCallbackFilePathTooLongFailure.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[invalidArgInstallDirPathTooLongFailure.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[invalidArgInstallDirPathTraversalFailure.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[invalidArgInstallWorkingDirPathNotSameFailure_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[invalidArgPatchDirPathTraversalFailure.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[invalidArgStageDirNotInInstallDirFailure_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[invalidArgWorkingDirPathLocalUNCFailure_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[invalidArgWorkingDirPathRelativeFailure.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[marSuccessComplete.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[marSuccessPartial.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[marFailurePartial.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[marStageSuccessComplete.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[marStageSuccessPartial.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[marVersionDowngrade.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 and mar signing +[marWrongChannel.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 and mar signing +[marStageFailurePartial.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[marCallbackAppSuccessComplete_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marCallbackAppSuccessPartial_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marCallbackAppStageSuccessComplete_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marCallbackAppStageSuccessPartial_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marAppInUseSuccessComplete.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[marAppInUseStageSuccessComplete_unix.js] +skip-if = os == 'win' +reason = not a Windows test +[marAppInUseStageFailureComplete_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marFileLockedFailureComplete_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marFileLockedFailurePartial_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marFileLockedStageFailureComplete_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marFileLockedStageFailurePartial_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marFileInUseSuccessComplete_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marFileInUseSuccessPartial_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marRMRFDirFileInUseSuccessComplete_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marRMRFDirFileInUseSuccessPartial_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marFileInUseStageFailureComplete_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marFileInUseStageFailurePartial_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marRMRFDirFileInUseStageFailureComplete_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marRMRFDirFileInUseStageFailurePartial_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marAppApplyDirLockedStageFailure_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marAppApplyUpdateAppBinInUseStageSuccess_win.js] +skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2') +reason = Windows only test and bug 1291985 +[marAppApplyUpdateSuccess.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[marAppApplyUpdateStageSuccess.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +[marAppApplyUpdateStageOldVersionFailure.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 diff --git a/toolkit/mozapps/update/tests/unit_service_updater/.eslintrc.js b/toolkit/mozapps/update/tests/unit_service_updater/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js new file mode 100644 index 000000000..015fbd0cb --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Bootstrap the tests using the service by installing our own version of the service */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + // We don't actually care if the MAR has any data, we only care about the + // application return code and update.status result. + gTestFiles = gTestFilesCommon; + gTestDirs = []; + setupUpdaterTest(FILE_COMPLETE_MAR, null); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdateUsingService finishes. + */ +function runUpdateFinished() { + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + + // We need to check the service log even though this is a bootstrap + // because the app bin could be in use by this test by the time the next + // test runs. + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js new file mode 100644 index 000000000..bf765ee78 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * We skip authenticode cert checks from the service udpates + * so that we can use updater-xpcshell with the wrong certs for testing. + * This tests that code path. */ + +function run_test() { + if (!IS_AUTHENTICODE_CHECK_ENABLED) { + return; + } + + let binDir = getGREBinDir(); + let maintenanceServiceBin = binDir.clone(); + maintenanceServiceBin.append(FILE_MAINTENANCE_SERVICE_BIN); + + let updaterBin = binDir.clone(); + updaterBin.append(FILE_UPDATER_BIN); + + debugDump("Launching maintenance service bin: " + + maintenanceServiceBin.path + " to check updater: " + + updaterBin.path + " signature."); + + // Bypass the manifest and run as invoker + gEnv.set("__COMPAT_LAYER", "RunAsInvoker"); + + let dummyInstallPath = "---"; + let maintenanceServiceBinArgs = ["check-cert", dummyInstallPath, + updaterBin.path]; + let maintenanceServiceBinProcess = Cc["@mozilla.org/process/util;1"]. + createInstance(Ci.nsIProcess); + maintenanceServiceBinProcess.init(maintenanceServiceBin); + maintenanceServiceBinProcess.run(true, maintenanceServiceBinArgs, + maintenanceServiceBinArgs.length); + Assert.equal(maintenanceServiceBinProcess.exitValue, 0, + "the maintenance service exit value should be 0"); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/head_update.js b/toolkit/mozapps/update/tests/unit_service_updater/head_update.js new file mode 100644 index 000000000..38be4ee39 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/head_update.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const IS_SERVICE_TEST = true; + +/* import-globals-from ../data/xpcshellUtilsAUS.js */ +load("../data/xpcshellUtilsAUS.js"); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js new file mode 100644 index 000000000..70e03646a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js @@ -0,0 +1,47 @@ +/* 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/. + */ + +/* Too long install directory path failure test */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR + : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = "123456789"; + if (IS_WIN) { + path = "\\" + path; + path = path.repeat(30); // 300 characters + path = "C:" + path; + } else { + path = "/" + path; + path = path.repeat(1000); // 10000 characters + } + + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js new file mode 100644 index 000000000..330578de6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js @@ -0,0 +1,44 @@ +/* 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/. + */ + +/* Install directory path traversal failure test */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR + : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = "123456789"; + if (IS_WIN) { + path = "C:\\" + path + "\\..\\" + path; + } else { + path = "/" + path + "/../" + path; + } + + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js new file mode 100644 index 000000000..8ddb34af0 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js @@ -0,0 +1,38 @@ +/* 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/. + */ + +/* Different install and working directories for a regular update failure */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_ERROR + : STATE_FAILED_INVALID_APPLYTO_DIR_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = getApplyDirFile("..", false).path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js new file mode 100644 index 000000000..c8ae3f0c6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js @@ -0,0 +1,43 @@ +/* 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/. + */ + +/* Patch directory path traversal failure test */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = getUpdatesPatchDir(); + if (IS_WIN) { + path = path + "\\..\\"; + } else { + path = path + "/../"; + } + + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, path, null, null, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js new file mode 100644 index 000000000..e9b227657 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js @@ -0,0 +1,38 @@ +/* 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/. + */ + +/* Different install and working directories for a regular update failure */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR + : STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = getApplyDirFile("..", false).path; + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true, null, null, path, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js new file mode 100644 index 000000000..87bbad4aa --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js @@ -0,0 +1,38 @@ +/* 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/. + */ + +/* Working directory path local UNC failure test */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR + : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + let path = "\\\\.\\" + getApplyDirFile(null, false).path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js new file mode 100644 index 000000000..a550909b2 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js @@ -0,0 +1,37 @@ +/* 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/. + */ + +/* Relative working directory path failure test */ + +const STATE_AFTER_RUNUPDATE = + IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR + : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, "test", null); +} + +/** + * Called after the call to runUpdateUsingUpdater finishes. + */ +function runUpdateFinished() { + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js new file mode 100644 index 000000000..b9f793236 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +const STATE_AFTER_STAGE = STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + createUpdateInProgressLockFile(getAppBaseDir()); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(false); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + removeUpdateInProgressLockFile(getAppBaseDir()); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(PERFORMING_STAGED_UPDATE); + checkUpdateLogContains(ERR_UPDATE_IN_PROGRESS); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js new file mode 100644 index 000000000..a606720b7 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, undefined); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true, false); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + lockDirectory(getAppBaseDir().path); + // Switch the application to the staged application that was updated. + runUpdateUsingApp(STATE_SUCCEEDED); +} + +/** + * Called after the call to runUpdateUsingApp finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + + let updatesDir = getUpdatesPatchDir(); + Assert.ok(updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path)); + + let log = getUpdateLog(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js new file mode 100644 index 000000000..5b9b08156 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, true); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdateUsingApp(STATE_SUCCEEDED); +} + +/** + * Called after the call to runUpdateUsingApp finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + + let updatesDir = getUpdatesPatchDir(); + Assert.ok(updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path)); + + let log = getUpdateLog(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js new file mode 100644 index 000000000..e76233fe6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + // The third parameter will test that a full path to the post update binary + // doesn't execute. + setupUpdaterTest(FILE_COMPLETE_MAR, undefined, + getApplyDirFile(null, true).path + "/"); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdateUsingApp(STATE_SUCCEEDED); +} + +/** + * Called after the call to runUpdateUsingApp finishes. + */ +function runUpdateFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + + let updatesDir = getUpdatesPatchDir(); + Assert.ok(updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path)); + + let log = getUpdateLog(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), + MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateLog(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..b1505d58e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file staged patch apply failure test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; +const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js new file mode 100644 index 000000000..93333cade --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js new file mode 100644 index 000000000..79e54c182 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Replace app binary complete MAR file staged patch apply success test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + gCallbackBinFile = "exe0.exe"; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js new file mode 100644 index 000000000..b1f84715f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Patch app binary partial MAR file staged patch apply success test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + gCallbackBinFile = "exe0.exe"; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js new file mode 100644 index 000000000..85e92d290 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Replace app binary complete MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + gCallbackBinFile = "exe0.exe"; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js new file mode 100644 index 000000000..1212c9ba2 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Patch app binary partial MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + gCallbackBinFile = "exe0.exe"; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js new file mode 100644 index 000000000..960c96f7b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js @@ -0,0 +1,47 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Failure Test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[11].originalFile = "partial.png"; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + // If execv is used the updater process will turn into the callback process + // and the updater's return code will be that of the callback process. + runUpdate(STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE, false, (USE_EXECV ? 0 : 1), + true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusFile(), STATE_NONE, + "the status file failure code" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, LOADSOURCE_ERROR_WRONG_SIZE, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_FAILURE); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..b39595f92 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file staged patch apply failure test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; +const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestFiles[13].relPathDir + gTestFiles[13].fileName, + false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js new file mode 100644 index 000000000..06d386ad6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file staged patch apply failure test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; +const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestFiles[11].relPathDir + gTestFiles[11].fileName, + false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js new file mode 100644 index 000000000..89a2fff5e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestFiles[13].relPathDir + gTestFiles[13].fileName, + false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js new file mode 100644 index 000000000..ea85ddccc --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestFiles[11].relPathDir + gTestFiles[11].fileName, + false); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js new file mode 100644 index 000000000..c5efaa8c0 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file patch apply failure test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperLockFile(gTestFiles[3]); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusFile(), STATE_PENDING, + "the status file failure code" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_PENDING, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, WRITE_ERROR, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_BACKUP_CREATE_7); + checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js new file mode 100644 index 000000000..4fdbadb5b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file patch apply failure test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperLockFile(gTestFiles[2]); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_FAILED_READ_ERROR, false, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusFile(), STATE_NONE, + "the status file failure code" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, READ_ERROR, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_UNABLE_OPEN_DEST); + checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..4d12f4e42 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file staged patch apply failure test */ + +const STATE_AFTER_STAGE = STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperLockFile(gTestFiles[3]); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + // Files aren't checked after staging since this test locks a file which + // prevents reading the file. + checkUpdateLogContains(ERR_ENSURE_COPY); + // Switch the application to the staged application that was updated. + runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, false); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusFile(), STATE_PENDING, + "the status file failure code" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 2, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_PENDING, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, WRITE_ERROR, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_BACKUP_CREATE_7); + checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js new file mode 100644 index 000000000..5f64df34c --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file staged patch apply failure test */ + +const STATE_AFTER_STAGE = STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperLockFile(gTestFiles[2]); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + // Files aren't checked after staging since this test locks a file which + // prevents reading the file. + checkUpdateLogContains(ERR_ENSURE_COPY); + // Switch the application to the staged application that was updated. + runUpdate(STATE_FAILED_READ_ERROR, false, 1, false); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusFile(), STATE_NONE, + "the status file failure code" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.updateCount, 2, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED, + "the update state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, READ_ERROR, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_UNABLE_OPEN_DEST); + checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..b83bafccc --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file staged patch apply failure + test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; +const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestDirs[4].relPathDir + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0], true); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js new file mode 100644 index 000000000..39ea485cd --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file staged patch apply failure + test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; +const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestDirs[2].relPathDir + gTestDirs[2].files[0], true); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js new file mode 100644 index 000000000..a71bb8d49 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestDirs[4].relPathDir + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0], true); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js new file mode 100644 index 000000000..2cbe70ed8 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file patch apply success test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runHelperFileInUse(gTestDirs[2].relPathDir + gTestDirs[2].files[0], true); +} + +/** + * Called after the call to waitForHelperSleep finishes. + */ +function waitForHelperSleepFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + waitForHelperExit(); +} + +/** + * Called after the call to waitForHelperExit finishes. + */ +function waitForHelperExitFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js new file mode 100644 index 000000000..a9ce23420 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js @@ -0,0 +1,41 @@ +/* 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/. + */ + +/* General Partial MAR File Staged Patch Apply Failure Test */ + +const STATE_AFTER_STAGE = STATE_FAILED; +gStagingRemovedUpdate = true; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[11].originalFile = "partial.png"; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, LOADSOURCE_ERROR_WRONG_SIZE, + "the update errorCode" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_LOADSOURCEFILE_FAILED); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js new file mode 100644 index 000000000..f7745f68f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js @@ -0,0 +1,132 @@ +/* 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/. + */ + +/* General Complete MAR File Staged Patch Apply Test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsCompleteSuccess; + setupDistributionDir(); + setupSymLinks(); + setupUpdaterTest(FILE_COMPLETE_MAR, false); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + checkSymLinks(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + checkDistributionDir(); + checkCallbackLog(); +} + +/** + * Setup the state of the distribution directory for the test. + */ +function setupDistributionDir() { + if (IS_MACOSX) { + // Create files in the old distribution directory location to verify that + // the directory and its contents are removed when there is a distribution + // directory in the new location. + let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true); + writeFile(testFile, "test\n"); + testFile = getApplyDirFile(DIR_MACOS + "distribution/test1/testFile", true); + writeFile(testFile, "test\n"); + } +} + +/** + * Checks the state of the distribution directory for the test. + */ +function checkDistributionDir() { + if (IS_MACOSX) { + let distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + checkUpdateLogContains(REMOVE_OLD_DIST_DIR); + } +} + +/** + * Setup symlinks for the test. + */ +function setupSymLinks() { + // Don't test symlinks on Mac OS X in this test since it tends to timeout. + // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js + if (IS_UNIX && !IS_MACOSX) { + removeSymlink(); + createSymlink(); + do_register_cleanup(removeSymlink); + gTestFiles.splice(gTestFiles.length - 3, 0, + { + description: "Readable symlink", + fileName: "link", + relPathDir: DIR_RESOURCES, + originalContents: "test", + compareContents: "test", + originalFile: null, + compareFile: null, + originalPerms: 0o666, + comparePerms: 0o666 + }); + } +} + +/** + * Checks the state of the symlinks for the test. + */ +function checkSymLinks() { + // Don't test symlinks on Mac OS X in this test since it tends to timeout. + // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js + if (IS_UNIX && !IS_MACOSX) { + checkSymlink(); + } +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js new file mode 100644 index 000000000..ef15326de --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js @@ -0,0 +1,112 @@ +/* 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/. + */ + +/* General Partial MAR File Staged Patch Apply Test */ + +const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED; + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[gTestFiles.length - 2].originalContents = null; + gTestFiles[gTestFiles.length - 2].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 2].comparePerms = 0o644; + gTestDirs = gTestDirsPartialSuccess; + preventDistributionFiles(); + setupDistributionDir(); + setupUpdaterTest(FILE_PARTIAL_MAR, true); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + stageUpdate(true); +} + +/** + * Called after the call to stageUpdate finishes. + */ +function stageUpdateFinished() { + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true, false, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true, true); + checkDistributionDir(); + checkCallbackLog(); +} + +/** + * Setup the state of the distribution directory for the test. + */ +function setupDistributionDir() { + if (IS_MACOSX) { + // Create files in the old distribution directory location to verify that + // the directory and its contents are moved to the new location on update. + let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true); + writeFile(testFile, "test\n"); + testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true); + writeFile(testFile, "test\n"); + } +} + +/** + * Checks the state of the distribution directory. + */ +function checkDistributionDir() { + let distributionDir = getApplyDirFile(DIR_RESOURCES + "distribution", true); + if (IS_MACOSX) { + Assert.ok(distributionDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(distributionDir.path)); + + let testFile = getApplyDirFile(DIR_RESOURCES + "distribution/testFile", true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + + testFile = getApplyDirFile(DIR_RESOURCES + "distribution/test/testFile", true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + + distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + + checkUpdateLogContains(MOVE_OLD_DIST_DIR); + } else { + debugDump("testing that files aren't added with an add-if instruction " + + "when the file's destination directory doesn't exist"); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + } +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js new file mode 100644 index 000000000..1008e867f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js @@ -0,0 +1,96 @@ +/* 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/. + */ + +/* General Complete MAR File Patch Apply Test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + preventDistributionFiles(); + setupDistributionDir(); + setupUpdaterTest(FILE_COMPLETE_MAR, true); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkPostUpdateAppLog(); +} + +/** + * Called after the call to checkPostUpdateAppLog finishes. + */ +function checkPostUpdateAppLogFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, false, false, true); + checkDistributionDir(); + checkCallbackLog(); +} + +/** + * Setup the state of the distribution directory for the test. + */ +function setupDistributionDir() { + if (IS_MACOSX) { + // Create files in the old distribution directory location to verify that + // the directory and its contents are moved to the new location on update. + let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true); + writeFile(testFile, "test\n"); + testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true); + writeFile(testFile, "test\n"); + } +} + +/** + * Checks the state of the distribution directory. + */ +function checkDistributionDir() { + let distributionDir = getApplyDirFile(DIR_RESOURCES + "distribution", true); + if (IS_MACOSX) { + Assert.ok(distributionDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(distributionDir.path)); + + let testFile = getApplyDirFile(DIR_RESOURCES + "distribution/testFile", true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + + testFile = getApplyDirFile(DIR_RESOURCES + "distribution/test/testFile", true); + Assert.ok(testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path)); + + distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + + checkUpdateLogContains(MOVE_OLD_DIST_DIR); + } else { + debugDump("testing that files aren't added with an add-if instruction " + + "when the file's destination directory doesn't exist"); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + } +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js new file mode 100644 index 000000000..616390f55 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js @@ -0,0 +1,79 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Test */ + +function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestFiles[gTestFiles.length - 2].originalContents = null; + gTestFiles[gTestFiles.length - 2].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 2].comparePerms = 0o644; + gTestDirs = gTestDirsPartialSuccess; + setupDistributionDir(); + // The third parameter will test that a relative path that contains a + // directory traversal to the post update binary doesn't execute. + setupUpdaterTest(FILE_PARTIAL_MAR, false, "test/../"); +} + +/** + * Called after the call to setupUpdaterTest finishes. + */ +function setupUpdaterTestFinished() { + runUpdate(STATE_SUCCEEDED, false, 0, true); +} + +/** + * Called after the call to runUpdate finishes. + */ +function runUpdateFinished() { + checkAppBundleModTime(); + standardInit(); + Assert.equal(readStatusState(), STATE_NONE, + "the status file state" + MSG_SHOULD_EQUAL); + Assert.ok(!gUpdateManager.activeUpdate, + "the active update should not be defined"); + Assert.equal(gUpdateManager.updateCount, 1, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL); + Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED, + "the update state" + MSG_SHOULD_EQUAL); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + checkDistributionDir(); + checkCallbackLog(); +} + +/** + * Setup the state of the distribution directory for the test. + */ +function setupDistributionDir() { + if (IS_MACOSX) { + // Create files in the old distribution directory location to verify that + // the directory and its contents are removed when there is a distribution + // directory in the new location. + let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true); + writeFile(testFile, "test\n"); + testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true); + writeFile(testFile, "test\n"); + } +} + +/** + * Checks the state of the distribution directory. + */ +function checkDistributionDir() { + if (IS_MACOSX) { + let distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true); + Assert.ok(!distributionDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path)); + checkUpdateLogContains(REMOVE_OLD_DIST_DIR); + } +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini b/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini new file mode 100644 index 000000000..1d63f8583 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini @@ -0,0 +1,156 @@ +; 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/. + +; Tests that require the updater binary and the maintenance service. + +[DEFAULT] +tags = appupdate +head = head_update.js +tail = + +[bootstrapSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgInstallDirPathTooLongFailureSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgInstallDirPathTraversalFailureSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgPatchDirPathTraversalFailureSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgStageDirNotInInstallDirFailureSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgWorkingDirPathLocalUNCFailureSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgWorkingDirPathRelativeFailureSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marSuccessCompleteSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marSuccessPartialSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marFailurePartialSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marStageSuccessCompleteSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marStageSuccessPartialSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marStageFailurePartialSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marCallbackAppSuccessCompleteSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marCallbackAppSuccessPartialSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marCallbackAppStageSuccessCompleteSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marCallbackAppStageSuccessPartialSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppInUseSuccessCompleteSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppInUseStageFailureCompleteSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedFailureCompleteSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedFailurePartialSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedStageFailureCompleteSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedStageFailurePartialSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseSuccessCompleteSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseSuccessPartialSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseSuccessCompleteSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseSuccessPartialSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseStageFailureCompleteSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseStageFailurePartialSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseStageFailureCompleteSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseStageFailurePartialSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyDirLockedStageFailureSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyUpdateSuccessSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyUpdateStageSuccessSvc.js] +skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2') +reason = bug 1291985 +run-sequentially = Uses the Mozilla Maintenance Service. +[checkUpdaterSigSvc.js] diff --git a/toolkit/mozapps/update/updater/Launchd.plist b/toolkit/mozapps/update/updater/Launchd.plist new file mode 100644 index 000000000..f0b5cef08 --- /dev/null +++ b/toolkit/mozapps/update/updater/Launchd.plist @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>org.mozilla.updater</string> + <key>RunAtLoad</key> + <true/> +</dict> +</plist> diff --git a/toolkit/mozapps/update/updater/Makefile.in b/toolkit/mozapps/update/updater/Makefile.in new file mode 100644 index 000000000..84a843d18 --- /dev/null +++ b/toolkit/mozapps/update/updater/Makefile.in @@ -0,0 +1,29 @@ +# vim:set ts=8 sw=8 sts=8 noet: +# 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/. + +# For changes here, also consider ./updater-xpcshell/Makefile.in + +ifndef MOZ_WINCONSOLE +ifdef MOZ_DEBUG +MOZ_WINCONSOLE = 1 +else +MOZ_WINCONSOLE = 0 +endif +endif + +include $(topsrcdir)/config/rules.mk + +ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) +export:: + sed -e 's/%MOZ_MACBUNDLE_ID%/$(MOZ_MACBUNDLE_ID)/' $(srcdir)/macbuild/Contents/Info.plist.in > $(DIST)/bin/Info.plist +libs:: + $(NSINSTALL) -D $(DIST)/bin/updater.app + rsync -a -C --exclude '*.in' $(srcdir)/macbuild/Contents $(DIST)/bin/updater.app + rsync -a -C $(DIST)/bin/Info.plist $(DIST)/bin/updater.app/Contents + sed -e 's/%APP_NAME%/$(MOZ_APP_DISPLAYNAME)/' $(srcdir)/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in | \ + iconv -f UTF-8 -t UTF-16 > $(DIST)/bin/updater.app/Contents/Resources/English.lproj/InfoPlist.strings + $(NSINSTALL) -D $(DIST)/bin/updater.app/Contents/MacOS + $(NSINSTALL) $(DIST)/bin/org.mozilla.updater $(DIST)/bin/updater.app/Contents/MacOS +endif diff --git a/toolkit/mozapps/update/updater/archivereader.cpp b/toolkit/mozapps/update/updater/archivereader.cpp new file mode 100644 index 000000000..90cf45c3d --- /dev/null +++ b/toolkit/mozapps/update/updater/archivereader.cpp @@ -0,0 +1,324 @@ +/* -*- 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 <string.h> +#include <stdlib.h> +#include <fcntl.h> +#include "bzlib.h" +#include "archivereader.h" +#include "errors.h" +#ifdef XP_WIN +#include "nsAlgorithm.h" // Needed by nsVersionComparator.cpp +#include "updatehelper.h" +#endif + +// These are generated at compile time based on the DER file for the channel +// being used +#ifdef MOZ_VERIFY_MAR_SIGNATURE +#ifdef TEST_UPDATER +#include "../xpcshellCert.h" +#else +#include "primaryCert.h" +#include "secondaryCert.h" +#endif +#endif + +#define UPDATER_NO_STRING_GLUE_STL +#include "nsVersionComparator.cpp" +#undef UPDATER_NO_STRING_GLUE_STL + +#if defined(XP_UNIX) +# include <sys/types.h> +#elif defined(XP_WIN) +# include <io.h> +#endif + +static int inbuf_size = 262144; +static int outbuf_size = 262144; +static char *inbuf = nullptr; +static char *outbuf = nullptr; + +/** + * Performs a verification on the opened MAR file with the passed in + * certificate name ID and type ID. + * + * @param archive The MAR file to verify the signature on. + * @param certData The certificate data. + * @return OK on success, CERT_VERIFY_ERROR on failure. +*/ +template<uint32_t SIZE> +int +VerifyLoadedCert(MarFile *archive, const uint8_t (&certData)[SIZE]) +{ + (void)archive; + (void)certData; + +#ifdef MOZ_VERIFY_MAR_SIGNATURE + const uint32_t size = SIZE; + const uint8_t* const data = &certData[0]; + if (mar_verify_signatures(archive, &data, &size, 1)) { + return CERT_VERIFY_ERROR; + } +#endif + + return OK; +} + +/** + * Performs a verification on the opened MAR file. Both the primary and backup + * keys stored are stored in the current process and at least the primary key + * will be tried. Success will be returned as long as one of the two + * signatures verify. + * + * @return OK on success +*/ +int +ArchiveReader::VerifySignature() +{ + if (!mArchive) { + return ARCHIVE_NOT_OPEN; + } + +#ifndef MOZ_VERIFY_MAR_SIGNATURE + return OK; +#else +#ifdef TEST_UPDATER + int rv = VerifyLoadedCert(mArchive, xpcshellCertData); +#else + int rv = VerifyLoadedCert(mArchive, primaryCertData); + if (rv != OK) { + rv = VerifyLoadedCert(mArchive, secondaryCertData); + } +#endif + return rv; +#endif +} + +/** + * Verifies that the MAR file matches the current product, channel, and version + * + * @param MARChannelID The MAR channel name to use, only updates from MARs + * with a matching MAR channel name will succeed. + * If an empty string is passed, no check will be done + * for the channel name in the product information block. + * If a comma separated list of values is passed then + * one value must match. + * @param appVersion The application version to use, only MARs with an + * application version >= to appVersion will be applied. + * @return OK on success + * COULD_NOT_READ_PRODUCT_INFO_BLOCK if the product info block + * could not be read. + * MARCHANNEL_MISMATCH_ERROR if update-settings.ini's MAR + * channel ID doesn't match the MAR + * file's MAR channel ID. + * VERSION_DOWNGRADE_ERROR if the application version for + * this updater is newer than the + * one in the MAR. + */ +int +ArchiveReader::VerifyProductInformation(const char *MARChannelID, + const char *appVersion) +{ + if (!mArchive) { + return ARCHIVE_NOT_OPEN; + } + + ProductInformationBlock productInfoBlock; + int rv = mar_read_product_info_block(mArchive, + &productInfoBlock); + if (rv != OK) { + return COULD_NOT_READ_PRODUCT_INFO_BLOCK_ERROR; + } + + // Only check the MAR channel name if specified, it should be passed in from + // the update-settings.ini file. + if (MARChannelID && strlen(MARChannelID)) { + // Check for at least one match in the comma separated list of values. + const char *delimiter = " ,\t"; + // Make a copy of the string in case a read only memory buffer + // was specified. strtok modifies the input buffer. + char channelCopy[512] = { 0 }; + strncpy(channelCopy, MARChannelID, sizeof(channelCopy) - 1); + char *channel = strtok(channelCopy, delimiter); + rv = MAR_CHANNEL_MISMATCH_ERROR; + while(channel) { + if (!strcmp(channel, productInfoBlock.MARChannelID)) { + rv = OK; + break; + } + channel = strtok(nullptr, delimiter); + } + } + + if (rv == OK) { + /* Compare both versions to ensure we don't have a downgrade + -1 if appVersion is older than productInfoBlock.productVersion + 1 if appVersion is newer than productInfoBlock.productVersion + 0 if appVersion is the same as productInfoBlock.productVersion + This even works with strings like: + - 12.0a1 being older than 12.0a2 + - 12.0a2 being older than 12.0b1 + - 12.0a1 being older than 12.0 + - 12.0 being older than 12.1a1 */ + int versionCompareResult = + mozilla::CompareVersions(appVersion, productInfoBlock.productVersion); + if (1 == versionCompareResult) { + rv = VERSION_DOWNGRADE_ERROR; + } + } + + free((void *)productInfoBlock.MARChannelID); + free((void *)productInfoBlock.productVersion); + return rv; +} + +int +ArchiveReader::Open(const NS_tchar *path) +{ + if (mArchive) + Close(); + + if (!inbuf) { + inbuf = (char *)malloc(inbuf_size); + if (!inbuf) { + // Try again with a smaller buffer. + inbuf_size = 1024; + inbuf = (char *)malloc(inbuf_size); + if (!inbuf) + return ARCHIVE_READER_MEM_ERROR; + } + } + + if (!outbuf) { + outbuf = (char *)malloc(outbuf_size); + if (!outbuf) { + // Try again with a smaller buffer. + outbuf_size = 1024; + outbuf = (char *)malloc(outbuf_size); + if (!outbuf) + return ARCHIVE_READER_MEM_ERROR; + } + } + +#ifdef XP_WIN + mArchive = mar_wopen(path); +#else + mArchive = mar_open(path); +#endif + if (!mArchive) + return READ_ERROR; + + return OK; +} + +void +ArchiveReader::Close() +{ + if (mArchive) { + mar_close(mArchive); + mArchive = nullptr; + } + + if (inbuf) { + free(inbuf); + inbuf = nullptr; + } + + if (outbuf) { + free(outbuf); + outbuf = nullptr; + } +} + +int +ArchiveReader::ExtractFile(const char *name, const NS_tchar *dest) +{ + const MarItem *item = mar_find_item(mArchive, name); + if (!item) + return READ_ERROR; + +#ifdef XP_WIN + FILE* fp = _wfopen(dest, L"wb+"); +#else + int fd = creat(dest, item->flags); + if (fd == -1) + return WRITE_ERROR; + + FILE *fp = fdopen(fd, "wb"); +#endif + if (!fp) + return WRITE_ERROR; + + int rv = ExtractItemToStream(item, fp); + + fclose(fp); + return rv; +} + +int +ArchiveReader::ExtractFileToStream(const char *name, FILE *fp) +{ + const MarItem *item = mar_find_item(mArchive, name); + if (!item) + return READ_ERROR; + + return ExtractItemToStream(item, fp); +} + +int +ArchiveReader::ExtractItemToStream(const MarItem *item, FILE *fp) +{ + /* decompress the data chunk by chunk */ + + bz_stream strm; + int offset, inlen, outlen, ret = OK; + + memset(&strm, 0, sizeof(strm)); + if (BZ2_bzDecompressInit(&strm, 0, 0) != BZ_OK) + return UNEXPECTED_BZIP_ERROR; + + offset = 0; + for (;;) { + if (!item->length) { + ret = UNEXPECTED_MAR_ERROR; + break; + } + + if (offset < (int) item->length && strm.avail_in == 0) { + inlen = mar_read(mArchive, item, offset, inbuf, inbuf_size); + if (inlen <= 0) + return READ_ERROR; + offset += inlen; + strm.next_in = inbuf; + strm.avail_in = inlen; + } + + strm.next_out = outbuf; + strm.avail_out = outbuf_size; + + ret = BZ2_bzDecompress(&strm); + if (ret != BZ_OK && ret != BZ_STREAM_END) { + ret = UNEXPECTED_BZIP_ERROR; + break; + } + + outlen = outbuf_size - strm.avail_out; + if (outlen) { + if (fwrite(outbuf, outlen, 1, fp) != 1) { + ret = WRITE_ERROR_EXTRACT; + break; + } + } + + if (ret == BZ_STREAM_END) { + ret = OK; + break; + } + } + + BZ2_bzDecompressEnd(&strm); + return ret; +} diff --git a/toolkit/mozapps/update/updater/archivereader.h b/toolkit/mozapps/update/updater/archivereader.h new file mode 100644 index 000000000..a9d78aab1 --- /dev/null +++ b/toolkit/mozapps/update/updater/archivereader.h @@ -0,0 +1,41 @@ +/* -*- 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/. */ + +#ifndef ArchiveReader_h__ +#define ArchiveReader_h__ + +#include <stdio.h> +#include "mar.h" + +#ifdef XP_WIN + typedef WCHAR NS_tchar; +#else + typedef char NS_tchar; +#endif + +// This class provides an API to extract files from an update archive. +class ArchiveReader +{ +public: + ArchiveReader() : mArchive(nullptr) {} + ~ArchiveReader() { Close(); } + + int Open(const NS_tchar *path); + int VerifySignature(); + int VerifyProductInformation(const char *MARChannelID, + const char *appVersion); + void Close(); + + int ExtractFile(const char *item, const NS_tchar *destination); + int ExtractFileToStream(const char *item, FILE *fp); + +private: + int ExtractItemToStream(const MarItem *item, FILE *fp); + + MarFile *mArchive; +}; + +#endif // ArchiveReader_h__ diff --git a/toolkit/mozapps/update/updater/automounter_gonk.cpp b/toolkit/mozapps/update/updater/automounter_gonk.cpp new file mode 100644 index 000000000..3dff2a133 --- /dev/null +++ b/toolkit/mozapps/update/updater/automounter_gonk.cpp @@ -0,0 +1,251 @@ +/* -*- 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 <android/log.h> +#include <cutils/android_reboot.h> +#include <errno.h> +#include <stdlib.h> +#include <sys/mount.h> +#include <sys/reboot.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> + +#include "automounter_gonk.h" +#include "updatedefines.h" +#include "updatelogging.h" + +#define LOG_TAG "GonkAutoMounter" + +#define GONK_LOG(level, format, ...) \ + LOG((LOG_TAG ": " format "\n", ##__VA_ARGS__)); \ + __android_log_print(level, LOG_TAG, format, ##__VA_ARGS__) + +#define LOGI(format, ...) GONK_LOG(ANDROID_LOG_INFO, format, ##__VA_ARGS__) +#define LOGE(format, ...) GONK_LOG(ANDROID_LOG_ERROR, format, ##__VA_ARGS__) + +const char *kGonkMountsPath = "/proc/mounts"; +const char *kGonkSystemPath = "/system"; + +GonkAutoMounter::GonkAutoMounter() : mDevice(nullptr), mAccess(Unknown) +{ + if (!RemountSystem(ReadWrite)) { + LOGE("Could not remount %s as read-write.", kGonkSystemPath); + } +} + +GonkAutoMounter::~GonkAutoMounter() +{ + bool result = RemountSystem(ReadOnly); + free(mDevice); + + if (!result) { + // Don't take any chances when remounting as read-only fails, just reboot. + Reboot(); + } +} + +void +GonkAutoMounter::Reboot() +{ + // The android_reboot wrapper provides more safety, doing fancier read-only + // remounting and attempting to sync() the filesystem first. If this fails + // our only hope is to force a reboot directly without these protections. + // For more, see system/core/libcutils/android_reboot.c + LOGE("Could not remount %s as read-only, forcing a system reboot.", + kGonkSystemPath); + LogFlush(); + + if (android_reboot(ANDROID_RB_RESTART, 0, nullptr) != 0) { + LOGE("Safe system reboot failed, attempting to force"); + LogFlush(); + + if (reboot(RB_AUTOBOOT) != 0) { + LOGE("CRITICAL: Failed to force restart"); + } + } +} + +static const char * +MountAccessToString(MountAccess access) +{ + switch (access) { + case ReadOnly: return "read-only"; + case ReadWrite: return "read-write"; + default: return "unknown"; + } +} + +bool +GonkAutoMounter::RemountSystem(MountAccess access) +{ + if (!UpdateMountStatus()) { + return false; + } + + if (mAccess == access) { + return true; + } + + unsigned long flags = MS_REMOUNT; + if (access == ReadOnly) { + flags |= MS_RDONLY; + // Give the system a chance to flush file buffers + sync(); + } + + if (!MountSystem(flags)) { + return false; + } + + // Check status again to verify /system has been properly remounted + if (!UpdateMountStatus()) { + return false; + } + + if (mAccess != access) { + LOGE("Updated mount status %s should be %s", + MountAccessToString(mAccess), + MountAccessToString(access)); + return false; + } + + return true; +} + +bool +GonkAutoMounter::UpdateMountStatus() +{ + FILE *mountsFile = NS_tfopen(kGonkMountsPath, "r"); + + if (mountsFile == nullptr) { + LOGE("Error opening %s: %s", kGonkMountsPath, strerror(errno)); + return false; + } + + // /proc/mounts returns a 0 size from fstat, so we use the same + // pre-allocated buffer size that ADB does here + const int mountsMaxSize = 4096; + char mountData[mountsMaxSize]; + size_t read = fread(mountData, 1, mountsMaxSize - 1, mountsFile); + mountData[read + 1] = '\0'; + + if (ferror(mountsFile)) { + LOGE("Error reading %s, %s", kGonkMountsPath, strerror(errno)); + fclose(mountsFile); + return false; + } + + char *token, *tokenContext; + bool foundSystem = false; + + for (token = strtok_r(mountData, "\n", &tokenContext); + token; + token = strtok_r(nullptr, "\n", &tokenContext)) + { + if (ProcessMount(token)) { + foundSystem = true; + break; + } + } + + fclose(mountsFile); + + if (!foundSystem) { + LOGE("Couldn't find %s mount in %s", kGonkSystemPath, kGonkMountsPath); + } + return foundSystem; +} + +bool +GonkAutoMounter::ProcessMount(const char *mount) +{ + const int strSize = 256; + char mountDev[strSize]; + char mountDir[strSize]; + char mountAccess[strSize]; + + int rv = sscanf(mount, "%255s %255s %*s %255s %*d %*d\n", + mountDev, mountDir, mountAccess); + mountDev[strSize - 1] = '\0'; + mountDir[strSize - 1] = '\0'; + mountAccess[strSize - 1] = '\0'; + + if (rv != 3) { + return false; + } + + if (strcmp(kGonkSystemPath, mountDir) != 0) { + return false; + } + + free(mDevice); + mDevice = strdup(mountDev); + mAccess = Unknown; + + char *option, *optionContext; + for (option = strtok_r(mountAccess, ",", &optionContext); + option; + option = strtok_r(nullptr, ",", &optionContext)) + { + if (strcmp("ro", option) == 0) { + mAccess = ReadOnly; + break; + } else if (strcmp("rw", option) == 0) { + mAccess = ReadWrite; + break; + } + } + + return true; +} + +/* + * Mark the given block device as read-write or read-only, using the BLKROSET + * ioctl. + */ +static void SetBlockReadWriteStatus(const char *blockdev, bool setReadOnly) { + int fd; + int roMode = setReadOnly ? 1 : 0; + + fd = open(blockdev, O_RDONLY); + if (fd < 0) { + return; + } + + if (ioctl(fd, BLKROSET, &roMode) == -1) { + LOGE("Error setting read-only mode on %s to %s: %s", blockdev, + setReadOnly ? "true": "false", strerror(errno)); + } + close(fd); +} + + +bool +GonkAutoMounter::MountSystem(unsigned long flags) +{ + if (!mDevice) { + LOGE("No device was found for %s", kGonkSystemPath); + return false; + } + + // Without setting the block device ro mode to false, we get a permission + // denied error while trying to remount it in read-write. + SetBlockReadWriteStatus(mDevice, (flags & MS_RDONLY)); + + const char *readOnly = flags & MS_RDONLY ? "read-only" : "read-write"; + int result = mount(mDevice, kGonkSystemPath, "none", flags, nullptr); + + if (result != 0) { + LOGE("Error mounting %s as %s: %s", kGonkSystemPath, readOnly, + strerror(errno)); + return false; + } + + LOGI("Mounted %s partition as %s", kGonkSystemPath, readOnly); + return true; +} diff --git a/toolkit/mozapps/update/updater/automounter_gonk.h b/toolkit/mozapps/update/updater/automounter_gonk.h new file mode 100644 index 000000000..e40cacbc2 --- /dev/null +++ b/toolkit/mozapps/update/updater/automounter_gonk.h @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +#ifndef AUTOMOUNTER_GONK_H__ +#define AUTOMOUNTER_GONK_H__ + +typedef enum { + ReadOnly, + ReadWrite, + Unknown +} MountAccess; + +/** + * This class will remount the /system partition as read-write in Gonk to allow + * the updater write access. Upon destruction, /system will be remounted back to + * read-only. If something causes /system to remain read-write, this class will + * reboot the device and allow the system to mount as read-only. + * + * Code inspired from AOSP system/core/adb/remount_service.c + */ +class GonkAutoMounter +{ +public: + GonkAutoMounter(); + ~GonkAutoMounter(); + + MountAccess GetAccess() const + { + return mAccess; + } + +private: + bool RemountSystem(MountAccess access); + bool ForceRemountReadOnly(); + bool UpdateMountStatus(); + bool ProcessMount(const char *mount); + bool MountSystem(unsigned long flags); + void Reboot(); + +private: + char *mDevice; + MountAccess mAccess; +}; + +#endif // AUTOMOUNTER_GONK_H__ diff --git a/toolkit/mozapps/update/updater/bspatch.cpp b/toolkit/mozapps/update/updater/bspatch.cpp new file mode 100644 index 000000000..e632fe3d3 --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch.cpp @@ -0,0 +1,187 @@ +/*- + * Copyright 2003,2004 Colin Percival + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * Changelog: + * 2005-04-26 - Define the header as a C structure, add a CRC32 checksum to + * the header, and make all the types 32-bit. + * --Benjamin Smedberg <benjamin@smedbergs.us> + */ + +#include "bspatch.h" +#include "errors.h" + +#include <sys/stat.h> +#include <stdlib.h> +#include <stdio.h> +#include <fcntl.h> +#include <string.h> +#include <limits.h> + +#if defined(XP_WIN) +# include <io.h> +#else +# include <unistd.h> +#endif + +#ifdef XP_WIN +# include <winsock2.h> +#else +# include <arpa/inet.h> +#endif + +#ifndef SSIZE_MAX +# define SSIZE_MAX LONG_MAX +#endif + +int +MBS_ReadHeader(FILE* file, MBSPatchHeader *header) +{ + size_t s = fread(header, 1, sizeof(MBSPatchHeader), file); + if (s != sizeof(MBSPatchHeader)) + return READ_ERROR; + + header->slen = ntohl(header->slen); + header->scrc32 = ntohl(header->scrc32); + header->dlen = ntohl(header->dlen); + header->cblen = ntohl(header->cblen); + header->difflen = ntohl(header->difflen); + header->extralen = ntohl(header->extralen); + + struct stat hs; + s = fstat(fileno(file), &hs); + if (s) + return READ_ERROR; + + if (memcmp(header->tag, "MBDIFF10", 8) != 0) + return UNEXPECTED_BSPATCH_ERROR; + + if (sizeof(MBSPatchHeader) + + header->cblen + + header->difflen + + header->extralen != uint32_t(hs.st_size)) + return UNEXPECTED_BSPATCH_ERROR; + + return OK; +} + +int +MBS_ApplyPatch(const MBSPatchHeader *header, FILE* patchFile, + unsigned char *fbuffer, FILE* file) +{ + unsigned char *fbufend = fbuffer + header->slen; + + unsigned char *buf = (unsigned char*) malloc(header->cblen + + header->difflen + + header->extralen); + if (!buf) + return BSPATCH_MEM_ERROR; + + int rv = OK; + + size_t r = header->cblen + header->difflen + header->extralen; + unsigned char *wb = buf; + while (r) { + const size_t count = (r > SSIZE_MAX) ? SSIZE_MAX : r; + size_t c = fread(wb, 1, count, patchFile); + if (c != count) { + rv = READ_ERROR; + goto end; + } + + r -= c; + wb += c; + } + + { + MBSPatchTriple *ctrlsrc = (MBSPatchTriple*) buf; + unsigned char *diffsrc = buf + header->cblen; + unsigned char *extrasrc = diffsrc + header->difflen; + + MBSPatchTriple *ctrlend = (MBSPatchTriple*) diffsrc; + unsigned char *diffend = extrasrc; + unsigned char *extraend = extrasrc + header->extralen; + + do { + ctrlsrc->x = ntohl(ctrlsrc->x); + ctrlsrc->y = ntohl(ctrlsrc->y); + ctrlsrc->z = ntohl(ctrlsrc->z); + +#ifdef DEBUG_bsmedberg + printf("Applying block:\n" + " x: %u\n" + " y: %u\n" + " z: %i\n", + ctrlsrc->x, + ctrlsrc->y, + ctrlsrc->z); +#endif + + /* Add x bytes from oldfile to x bytes from the diff block */ + + if (fbuffer + ctrlsrc->x > fbufend || + diffsrc + ctrlsrc->x > diffend) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + for (uint32_t i = 0; i < ctrlsrc->x; ++i) { + diffsrc[i] += fbuffer[i]; + } + if ((uint32_t) fwrite(diffsrc, 1, ctrlsrc->x, file) != ctrlsrc->x) { + rv = WRITE_ERROR_PATCH_FILE; + goto end; + } + fbuffer += ctrlsrc->x; + diffsrc += ctrlsrc->x; + + /* Copy y bytes from the extra block */ + + if (extrasrc + ctrlsrc->y > extraend) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + if ((uint32_t) fwrite(extrasrc, 1, ctrlsrc->y, file) != ctrlsrc->y) { + rv = WRITE_ERROR_PATCH_FILE; + goto end; + } + extrasrc += ctrlsrc->y; + + /* "seek" forwards in oldfile by z bytes */ + + if (fbuffer + ctrlsrc->z > fbufend) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + fbuffer += ctrlsrc->z; + + /* and on to the next control block */ + + ++ctrlsrc; + } while (ctrlsrc < ctrlend); + } + +end: + free(buf); + return rv; +} diff --git a/toolkit/mozapps/update/updater/bspatch.h b/toolkit/mozapps/update/updater/bspatch.h new file mode 100644 index 000000000..c24c001cc --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch.h @@ -0,0 +1,93 @@ +/*- + * Copyright 2003,2004 Colin Percival + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * Changelog: + * 2005-04-26 - Define the header as a C structure, add a CRC32 checksum to + * the header, and make all the types 32-bit. + * --Benjamin Smedberg <benjamin@smedbergs.us> + */ + +#ifndef bspatch_h__ +#define bspatch_h__ + +#include <stdint.h> +#include <stdio.h> + +typedef struct MBSPatchHeader_ { + /* "MBDIFF10" */ + char tag[8]; + + /* Length of the file to be patched */ + uint32_t slen; + + /* CRC32 of the file to be patched */ + uint32_t scrc32; + + /* Length of the result file */ + uint32_t dlen; + + /* Length of the control block in bytes */ + uint32_t cblen; + + /* Length of the diff block in bytes */ + uint32_t difflen; + + /* Length of the extra block in bytes */ + uint32_t extralen; + + /* Control block (MBSPatchTriple[]) */ + /* Diff block (binary data) */ + /* Extra block (binary data) */ +} MBSPatchHeader; + +/** + * Read the header of a patch file into the MBSPatchHeader structure. + * + * @param fd Must have been opened for reading, and be at the beginning + * of the file. + */ +int MBS_ReadHeader(FILE* file, MBSPatchHeader *header); + +/** + * Apply a patch. This method does not validate the checksum of the original + * file: client code should validate the checksum before calling this method. + * + * @param patchfd Must have been processed by MBS_ReadHeader + * @param fbuffer The original file read into a memory buffer of length + * header->slen. + * @param filefd Must have been opened for writing. Should be truncated + * to header->dlen if it is an existing file. The offset + * should be at the beginning of the file. + */ +int MBS_ApplyPatch(const MBSPatchHeader *header, FILE* patchFile, + unsigned char *fbuffer, FILE* file); + +typedef struct MBSPatchTriple_ { + uint32_t x; /* add x bytes from oldfile to x bytes from the diff block */ + uint32_t y; /* copy y bytes from the extra block */ + int32_t z; /* seek forwards in oldfile by z bytes */ +} MBSPatchTriple; + +#endif // bspatch_h__ diff --git a/toolkit/mozapps/update/updater/dep1.der b/toolkit/mozapps/update/updater/dep1.der Binary files differnew file mode 100644 index 000000000..95b4ef38c --- /dev/null +++ b/toolkit/mozapps/update/updater/dep1.der diff --git a/toolkit/mozapps/update/updater/dep2.der b/toolkit/mozapps/update/updater/dep2.der Binary files differnew file mode 100644 index 000000000..a460d6a16 --- /dev/null +++ b/toolkit/mozapps/update/updater/dep2.der diff --git a/toolkit/mozapps/update/updater/gen_cert_header.py b/toolkit/mozapps/update/updater/gen_cert_header.py new file mode 100644 index 000000000..7ecb15619 --- /dev/null +++ b/toolkit/mozapps/update/updater/gen_cert_header.py @@ -0,0 +1,22 @@ +from __future__ import print_function + +import binascii + +def file_byte_generator(filename, block_size = 512): + with open(filename, "rb") as f: + while True: + block = f.read(block_size) + if block: + for byte in block: + yield byte + else: + break + +def create_header(out_fh, in_filename): + assert out_fh.name.endswith('.h') + array_name = out_fh.name[:-2] + 'Data' + hexified = ["0x" + binascii.hexlify(byte) for byte in file_byte_generator(in_filename)] + print("const uint8_t " + array_name + "[] = {", file=out_fh) + print(", ".join(hexified), file=out_fh) + print("};", file=out_fh) + return 0 diff --git a/toolkit/mozapps/update/updater/launchchild_osx.mm b/toolkit/mozapps/update/updater/launchchild_osx.mm new file mode 100644 index 000000000..5a36ae623 --- /dev/null +++ b/toolkit/mozapps/update/updater/launchchild_osx.mm @@ -0,0 +1,384 @@ +/* -*- 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 <Cocoa/Cocoa.h> +#include <CoreServices/CoreServices.h> +#include <crt_externs.h> +#include <stdlib.h> +#include <stdio.h> +#include <spawn.h> +#include <SystemConfiguration/SystemConfiguration.h> +#include "readstrings.h" + +class MacAutoreleasePool { +public: + MacAutoreleasePool() + { + mPool = [[NSAutoreleasePool alloc] init]; + } + ~MacAutoreleasePool() + { + [mPool release]; + } + +private: + NSAutoreleasePool* mPool; +}; + +void LaunchChild(int argc, const char** argv) +{ + MacAutoreleasePool pool; + + @try { + NSString* launchPath = [NSString stringWithUTF8String:argv[0]]; + NSMutableArray* arguments = [NSMutableArray arrayWithCapacity:argc - 1]; + for (int i = 1; i < argc; i++) { + [arguments addObject:[NSString stringWithUTF8String:argv[i]]]; + } + [NSTask launchedTaskWithLaunchPath:launchPath + arguments:arguments]; + } @catch (NSException* e) { + NSLog(@"%@: %@", e.name, e.reason); + } +} + +void +LaunchMacPostProcess(const char* aAppBundle) +{ + MacAutoreleasePool pool; + + // Launch helper to perform post processing for the update; this is the Mac + // analogue of LaunchWinPostProcess (PostUpdateWin). + NSString* iniPath = [NSString stringWithUTF8String:aAppBundle]; + iniPath = + [iniPath stringByAppendingPathComponent:@"Contents/Resources/updater.ini"]; + + NSFileManager* fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:iniPath]) { + // the file does not exist; there is nothing to run + return; + } + + int readResult; + char values[2][MAX_TEXT_LEN]; + readResult = ReadStrings([iniPath UTF8String], + "ExeRelPath\0ExeArg\0", + 2, + values, + "PostUpdateMac"); + if (readResult) { + return; + } + + NSString *exeRelPath = [NSString stringWithUTF8String:values[0]]; + NSString *exeArg = [NSString stringWithUTF8String:values[1]]; + if (!exeArg || !exeRelPath) { + return; + } + + // The path must not traverse directories and it must be a relative path. + if ([exeRelPath rangeOfString:@".."].location != NSNotFound || + [exeRelPath rangeOfString:@"./"].location != NSNotFound || + [exeRelPath rangeOfString:@"/"].location == 0) { + return; + } + + NSString* exeFullPath = [NSString stringWithUTF8String:aAppBundle]; + exeFullPath = [exeFullPath stringByAppendingPathComponent:exeRelPath]; + + char optVals[1][MAX_TEXT_LEN]; + readResult = ReadStrings([iniPath UTF8String], + "ExeAsync\0", + 1, + optVals, + "PostUpdateMac"); + + NSTask *task = [[NSTask alloc] init]; + [task setLaunchPath:exeFullPath]; + [task setArguments:[NSArray arrayWithObject:exeArg]]; + [task launch]; + if (!readResult) { + NSString *exeAsync = [NSString stringWithUTF8String:optVals[0]]; + if ([exeAsync isEqualToString:@"false"]) { + [task waitUntilExit]; + } + } + // ignore the return value of the task, there's nothing we can do with it + [task release]; +} + +id ConnectToUpdateServer() +{ + MacAutoreleasePool pool; + + id updateServer = nil; + BOOL isConnected = NO; + int currTry = 0; + const int numRetries = 10; // Number of IPC connection retries before + // giving up. + while (!isConnected && currTry < numRetries) { + @try { + updateServer = (id)[NSConnection + rootProxyForConnectionWithRegisteredName: + @"org.mozilla.updater.server" + host:nil + usingNameServer:[NSSocketPortNameServer sharedInstance]]; + if (!updateServer || + ![updateServer respondsToSelector:@selector(abort)] || + ![updateServer respondsToSelector:@selector(getArguments)] || + ![updateServer respondsToSelector:@selector(shutdown)]) { + NSLog(@"Server doesn't exist or doesn't provide correct selectors."); + sleep(1); // Wait 1 second. + currTry++; + } else { + isConnected = YES; + } + } @catch (NSException* e) { + NSLog(@"Encountered exception, retrying: %@: %@", e.name, e.reason); + sleep(1); // Wait 1 second. + currTry++; + } + } + if (!isConnected) { + NSLog(@"Failed to connect to update server after several retries."); + return nil; + } + return updateServer; +} + +void CleanupElevatedMacUpdate(bool aFailureOccurred) +{ + MacAutoreleasePool pool; + + id updateServer = ConnectToUpdateServer(); + if (updateServer) { + @try { + if (aFailureOccurred) { + [updateServer performSelector:@selector(abort)]; + } else { + [updateServer performSelector:@selector(shutdown)]; + } + } @catch (NSException* e) { } + } + + NSFileManager* manager = [NSFileManager defaultManager]; + [manager removeItemAtPath:@"/Library/PrivilegedHelperTools/org.mozilla.updater" + error:nil]; + [manager removeItemAtPath:@"/Library/LaunchDaemons/org.mozilla.updater.plist" + error:nil]; + const char* launchctlArgs[] = {"/bin/launchctl", + "remove", + "org.mozilla.updater"}; + // The following call will terminate the current process due to the "remove" + // argument in launchctlArgs. + LaunchChild(3, launchctlArgs); +} + +// Note: Caller is responsible for freeing argv. +bool ObtainUpdaterArguments(int* argc, char*** argv) +{ + MacAutoreleasePool pool; + + id updateServer = ConnectToUpdateServer(); + if (!updateServer) { + // Let's try our best and clean up. + CleanupElevatedMacUpdate(true); + return false; // Won't actually get here due to CleanupElevatedMacUpdate. + } + + @try { + NSArray* updaterArguments = + [updateServer performSelector:@selector(getArguments)]; + *argc = [updaterArguments count]; + char** tempArgv = (char**)malloc(sizeof(char*) * (*argc)); + for (int i = 0; i < *argc; i++) { + int argLen = [[updaterArguments objectAtIndex:i] length] + 1; + tempArgv[i] = (char*)malloc(argLen); + strncpy(tempArgv[i], [[updaterArguments objectAtIndex:i] UTF8String], + argLen); + } + *argv = tempArgv; + } @catch (NSException* e) { + // Let's try our best and clean up. + CleanupElevatedMacUpdate(true); + return false; // Won't actually get here due to CleanupElevatedMacUpdate. + } + return true; +} + +/** + * The ElevatedUpdateServer is launched from a non-elevated updater process. + * It allows an elevated updater process (usually a privileged helper tool) to + * connect to it and receive all the necessary arguments to complete a + * successful update. + */ +@interface ElevatedUpdateServer : NSObject +{ + NSArray* mUpdaterArguments; + BOOL mShouldKeepRunning; + BOOL mAborted; +} +- (id)initWithArgs:(NSArray*)args; +- (BOOL)runServer; +- (NSArray*)getArguments; +- (void)abort; +- (BOOL)wasAborted; +- (void)shutdown; +- (BOOL)shouldKeepRunning; +@end + +@implementation ElevatedUpdateServer + +- (id)initWithArgs:(NSArray*)args +{ + self = [super init]; + if (!self) { + return nil; + } + mUpdaterArguments = args; + mShouldKeepRunning = YES; + mAborted = NO; + return self; +} + +- (BOOL)runServer +{ + NSPort* serverPort = [NSSocketPort port]; + NSConnection* server = [NSConnection connectionWithReceivePort:serverPort + sendPort:serverPort]; + [server setRootObject:self]; + if ([server registerName:@"org.mozilla.updater.server" + withNameServer:[NSSocketPortNameServer sharedInstance]] == NO) { + NSLog(@"Unable to register as DirectoryServer."); + NSLog(@"Is another copy running?"); + return NO; + } + + while ([self shouldKeepRunning] && + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode + beforeDate:[NSDate distantFuture]]); + return ![self wasAborted]; +} + +- (NSArray*)getArguments +{ + return mUpdaterArguments; +} + +- (void)abort +{ + mAborted = YES; + [self shutdown]; +} + +- (BOOL)wasAborted +{ + return mAborted; +} + +- (void)shutdown +{ + mShouldKeepRunning = NO; +} + +- (BOOL)shouldKeepRunning +{ + return mShouldKeepRunning; +} + +@end + +bool ServeElevatedUpdate(int argc, const char** argv) +{ + MacAutoreleasePool pool; + + NSMutableArray* updaterArguments = [NSMutableArray arrayWithCapacity:argc]; + for (int i = 0; i < argc; i++) { + [updaterArguments addObject:[NSString stringWithUTF8String:argv[i]]]; + } + + ElevatedUpdateServer* updater = + [[ElevatedUpdateServer alloc] initWithArgs:[updaterArguments copy]]; + bool didSucceed = [updater runServer]; + + [updater release]; + return didSucceed; +} + +bool IsOwnedByGroupAdmin(const char* aAppBundle) +{ + MacAutoreleasePool pool; + + NSString* appDir = [NSString stringWithUTF8String:aAppBundle]; + NSFileManager* fileManager = [NSFileManager defaultManager]; + + NSDictionary* attributes = [fileManager attributesOfItemAtPath:appDir + error:nil]; + bool isOwnedByAdmin = false; + if (attributes && + [[attributes valueForKey:NSFileGroupOwnerAccountID] intValue] == 80) { + isOwnedByAdmin = true; + } + return isOwnedByAdmin; +} + +void SetGroupOwnershipAndPermissions(const char* aAppBundle) +{ + MacAutoreleasePool pool; + + NSString* appDir = [NSString stringWithUTF8String:aAppBundle]; + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSError* error = nil; + NSArray* paths = + [fileManager subpathsOfDirectoryAtPath:appDir + error:&error]; + if (error) { + return; + } + + // Set group ownership of Firefox.app to 80 ("admin") and permissions to + // 0775. + if (![fileManager setAttributes:@{ NSFileGroupOwnerAccountID: @(80), + NSFilePosixPermissions: @(0775) } + ofItemAtPath:appDir + error:&error] || error) { + return; + } + + NSArray* permKeys = [NSArray arrayWithObjects:NSFileGroupOwnerAccountID, + NSFilePosixPermissions, + nil]; + // For all descendants of Firefox.app, set group ownership to 80 ("admin") and + // ensure write permission for the group. + for (NSString* currPath in paths) { + NSString* child = [appDir stringByAppendingPathComponent:currPath]; + NSDictionary* oldAttributes = + [fileManager attributesOfItemAtPath:child + error:&error]; + if (error) { + return; + } + // Skip symlinks, since they could be pointing to files outside of the .app + // bundle. + if ([oldAttributes fileType] == NSFileTypeSymbolicLink) { + continue; + } + NSNumber* oldPerms = + (NSNumber*)[oldAttributes valueForKey:NSFilePosixPermissions]; + NSArray* permObjects = + [NSArray arrayWithObjects: + [NSNumber numberWithUnsignedLong:80], + [NSNumber numberWithUnsignedLong:[oldPerms shortValue] | 020], + nil]; + NSDictionary* attributes = [NSDictionary dictionaryWithObjects:permObjects + forKeys:permKeys]; + if (![fileManager setAttributes:attributes + ofItemAtPath:child + error:&error] || error) { + return; + } + } +} diff --git a/toolkit/mozapps/update/updater/loaddlls.cpp b/toolkit/mozapps/update/updater/loaddlls.cpp new file mode 100644 index 000000000..b4291a5df --- /dev/null +++ b/toolkit/mozapps/update/updater/loaddlls.cpp @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 <windows.h> + +// Delayed load libraries are loaded when the first symbol is used. +// The following ensures that we load the delayed loaded libraries from the +// system directory. +struct AutoLoadSystemDependencies +{ + AutoLoadSystemDependencies() + { + // Remove the current directory from the search path for dynamically loaded + // DLLs as a precaution. This call has no effect for delay load DLLs. + SetDllDirectory(L""); + + HMODULE module = ::GetModuleHandleW(L"kernel32.dll"); + if (module) { + // SetDefaultDllDirectories is always available on Windows 8 and above. It + // is also available on Windows Vista, Windows Server 2008, and + // Windows 7 when MS KB2533623 has been applied. + decltype(SetDefaultDllDirectories)* setDefaultDllDirectories = + (decltype(SetDefaultDllDirectories)*) GetProcAddress(module, "SetDefaultDllDirectories"); + if (setDefaultDllDirectories) { + setDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32); + return; + } + } + + // When SetDefaultDllDirectories is not available, fallback to preloading + // dlls. The order that these are loaded does not matter since they are + // loaded using the LOAD_WITH_ALTERED_SEARCH_PATH flag. +#ifdef HAVE_64BIT_BUILD + // DLLs for Firefox x64 on Windows 7 (x64). + // Note: dwmapi.dll is preloaded since a crash will try to load it from the + // application's directory. + static LPCWSTR delayDLLs[] = { L"apphelp.dll", + L"cryptbase.dll", + L"cryptsp.dll", + L"dwmapi.dll", + L"mpr.dll", + L"ntmarta.dll", + L"profapi.dll", + L"propsys.dll", + L"sspicli.dll", + L"wsock32.dll" }; + +#else + // DLLs for Firefox x86 on Windows XP through Windows 7 (x86 and x64). + // Note: dwmapi.dll is preloaded since a crash will try to load it from the + // application's directory. + static LPCWSTR delayDLLs[] = { L"apphelp.dll", + L"crypt32.dll", + L"cryptbase.dll", + L"cryptsp.dll", + L"dwmapi.dll", + L"mpr.dll", + L"msasn1.dll", + L"ntmarta.dll", + L"profapi.dll", + L"propsys.dll", + L"psapi.dll", + L"secur32.dll", + L"sspicli.dll", + L"userenv.dll", + L"uxtheme.dll", + L"ws2_32.dll", + L"ws2help.dll", + L"wsock32.dll" }; +#endif + + WCHAR systemDirectory[MAX_PATH + 1] = { L'\0' }; + // If GetSystemDirectory fails we accept that we'll load the DLLs from the + // normal search path. + GetSystemDirectoryW(systemDirectory, MAX_PATH + 1); + size_t systemDirLen = wcslen(systemDirectory); + + // Make the system directory path terminate with a slash + if (systemDirectory[systemDirLen - 1] != L'\\' && systemDirLen) { + systemDirectory[systemDirLen] = L'\\'; + ++systemDirLen; + // No need to re-null terminate + } + + // For each known DLL ensure it is loaded from the system32 directory + for (size_t i = 0; i < sizeof(delayDLLs) / sizeof(delayDLLs[0]); ++i) { + size_t fileLen = wcslen(delayDLLs[i]); + wcsncpy(systemDirectory + systemDirLen, delayDLLs[i], + MAX_PATH - systemDirLen); + if (systemDirLen + fileLen <= MAX_PATH) { + systemDirectory[systemDirLen + fileLen] = L'\0'; + } else { + systemDirectory[MAX_PATH] = L'\0'; + } + LPCWSTR fullModulePath = systemDirectory; // just for code readability + // LOAD_WITH_ALTERED_SEARCH_PATH makes a dll look in its own directory for + // dependencies and is only available on Win 7 and below. + LoadLibraryExW(fullModulePath, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + } + } +} loadDLLs; diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in b/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in new file mode 100644 index 000000000..a9b9fcba9 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleExecutable</key> + <string>org.mozilla.updater</string> + <key>CFBundleIconFile</key> + <string>updater.icns</string> + <key>CFBundleIdentifier</key> + <string>org.mozilla.updater</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1.0</string> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> + <key>LSMinimumSystemVersion</key> + <string>10.5</string> + <key>LSMinimumSystemVersionByArchitecture</key> + <dict> + <key>i386</key> + <string>10.5.0</string> + <key>x86_64</key> + <string>10.6.0</string> + </dict> + <key>SMAuthorizedClients</key> + <array> + <string>identifier "%MOZ_MACBUNDLE_ID%" and ((anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9]) or (anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] and certificate leaf[field.1.2.840.113635.100.6.1.13] and certificate leaf[subject.OU] = "43AQ936H96"))</string> + </array> +</dict> +</plist> diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/PkgInfo b/toolkit/mozapps/update/updater/macbuild/Contents/PkgInfo new file mode 100644 index 000000000..bd04210fb --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/PkgInfo @@ -0,0 +1 @@ +APPL????
\ No newline at end of file diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in new file mode 100644 index 000000000..bca4022e7 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in @@ -0,0 +1,7 @@ +/* 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/. */ + +/* Localized versions of Info.plist keys */ + +CFBundleName = "%APP_NAME% Software Update"; diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib new file mode 100644 index 000000000..6cfb50406 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,19 @@ +{ + IBClasses = ( + { + CLASS = FirstResponder; + LANGUAGE = ObjC; + SUPERCLASS = NSObject; +}, + { + CLASS = UpdaterUI; + LANGUAGE = ObjC; + OUTLETS = { + progressBar = NSProgressIndicator; + progressTextField = NSTextField; + }; + SUPERCLASS = NSObject; +} + ); + IBVersion = 1; +}
\ No newline at end of file diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib new file mode 100644 index 000000000..150917837 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IBDocumentLocation</key> + <string>111 162 356 240 0 0 1440 878 </string> + <key>IBEditorPositions</key> + <dict> + <key>29</key> + <string>106 299 84 44 0 0 1440 878 </string> + </dict> + <key>IBFramework Version</key> + <string>489.0</string> + <key>IBOpenObjects</key> + <array> + <integer>21</integer> + <integer>29</integer> + </array> + <key>IBSystem Version</key> + <string>10J567</string> +</dict> +</plist> diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib Binary files differnew file mode 100644 index 000000000..61ff02600 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns Binary files differnew file mode 100644 index 000000000..d7499c669 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns diff --git a/toolkit/mozapps/update/updater/module.ver b/toolkit/mozapps/update/updater/module.ver new file mode 100644 index 000000000..771416bb1 --- /dev/null +++ b/toolkit/mozapps/update/updater/module.ver @@ -0,0 +1 @@ +WIN32_MODULE_DESCRIPTION=@MOZ_APP_DISPLAYNAME@ Software Updater diff --git a/toolkit/mozapps/update/updater/moz.build b/toolkit/mozapps/update/updater/moz.build new file mode 100644 index 000000000..1cca83b5b --- /dev/null +++ b/toolkit/mozapps/update/updater/moz.build @@ -0,0 +1,62 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa': + Program('org.mozilla.updater') +else: + Program('updater') + +updater_rel_path = '' +include('updater-common.build') +if CONFIG['ENABLE_TESTS']: + DIRS += ['updater-xpcshell'] + +CXXFLAGS += CONFIG['MOZ_BZ2_CFLAGS'] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa': + LDFLAGS += ['-sectcreate', + '__TEXT', + '__info_plist', + TOPOBJDIR + '/dist/bin/Info.plist', + '-sectcreate', + '__TEXT', + '__launchd_plist', + SRCDIR + '/Launchd.plist'] + +GENERATED_FILES = [ + 'primaryCert.h', + 'secondaryCert.h', + 'xpcshellCert.h', +] + +primary_cert = GENERATED_FILES['primaryCert.h'] +secondary_cert = GENERATED_FILES['secondaryCert.h'] + +# This is how the xpcshellCertificate.der file is generated, in case we ever +# have to regenerate it. +# ./certutil -L -d modules/libmar/tests/unit/data -n mycert -r > xpcshellCertificate.der +xpcshell_cert = GENERATED_FILES['xpcshellCert.h'] + +primary_cert.script = 'gen_cert_header.py:create_header' +secondary_cert.script = 'gen_cert_header.py:create_header' +xpcshell_cert.script = 'gen_cert_header.py:create_header' + +if CONFIG['MOZ_UPDATE_CHANNEL'] in ('beta', 'release', 'esr'): + primary_cert.inputs += ['release_primary.der'] + secondary_cert.inputs += ['release_secondary.der'] +elif CONFIG['MOZ_UPDATE_CHANNEL'] in ('nightly', 'aurora', 'nightly-elm', + 'nightly-profiling', 'nightly-oak', + 'nightly-ux'): + primary_cert.inputs += ['nightly_aurora_level3_primary.der'] + secondary_cert.inputs += ['nightly_aurora_level3_secondary.der'] +else: + primary_cert.inputs += ['dep1.der'] + secondary_cert.inputs += ['dep2.der'] + +xpcshell_cert.inputs += ['xpcshellCertificate.der'] + +if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: + FINAL_TARGET_FILES.icons += ['updater.png'] diff --git a/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der b/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der Binary files differnew file mode 100644 index 000000000..b22124798 --- /dev/null +++ b/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der diff --git a/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der b/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der Binary files differnew file mode 100644 index 000000000..2dffbd02d --- /dev/null +++ b/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der diff --git a/toolkit/mozapps/update/updater/progressui.h b/toolkit/mozapps/update/updater/progressui.h new file mode 100644 index 000000000..5462815de --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui.h @@ -0,0 +1,40 @@ +/* -*- 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/. */ + +#ifndef PROGRESSUI_H__ +#define PROGRESSUI_H__ + +#include "updatedefines.h" + +#if defined(XP_WIN) + typedef WCHAR NS_tchar; + #define NS_main wmain +#else + typedef char NS_tchar; + #define NS_main main +#endif + +// Called to perform any initialization of the widget toolkit +int InitProgressUI(int *argc, NS_tchar ***argv); + +#if defined(XP_WIN) + // Called on the main thread at startup + int ShowProgressUI(bool indeterminate = false, bool initUIStrings = true); + int InitProgressUIStrings(); +#elif defined(XP_MACOSX) + // Called on the main thread at startup + int ShowProgressUI(bool indeterminate = false); +#else + // Called on the main thread at startup + int ShowProgressUI(); +#endif +// May be called from any thread +void QuitProgressUI(); + +// May be called from any thread: progress is a number between 0 and 100 +void UpdateProgressUI(float progress); + +#endif // PROGRESSUI_H__ diff --git a/toolkit/mozapps/update/updater/progressui_gonk.cpp b/toolkit/mozapps/update/updater/progressui_gonk.cpp new file mode 100644 index 000000000..f77d0af63 --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_gonk.cpp @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=8 et : + */ +/* 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 <assert.h> +#include <stdio.h> + +#include <string> + +#include "android/log.h" + +#include "progressui.h" + +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "GeckoUpdater" , ## args) + +using namespace std; + +int InitProgressUI(int *argc, char ***argv) +{ + return 0; +} + +int ShowProgressUI() +{ + LOG("Starting to apply update ...\n"); + return 0; +} + +void QuitProgressUI() +{ + LOG("Finished applying update\n"); +} + +void UpdateProgressUI(float progress) +{ + assert(0.0f <= progress && progress <= 100.0f); + + static const size_t kProgressBarLength = 50; + static size_t sLastNumBars; + size_t numBars = size_t(float(kProgressBarLength) * progress / 100.0f); + if (numBars == sLastNumBars) { + return; + } + sLastNumBars = numBars; + + size_t numSpaces = kProgressBarLength - numBars; + string bars(numBars, '='); + string spaces(numSpaces, ' '); + LOG("Progress [ %s%s ]\n", bars.c_str(), spaces.c_str()); +} diff --git a/toolkit/mozapps/update/updater/progressui_gtk.cpp b/toolkit/mozapps/update/updater/progressui_gtk.cpp new file mode 100644 index 000000000..902bc5ac8 --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_gtk.cpp @@ -0,0 +1,132 @@ +/* -*- 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 <stdio.h> +#include <gtk/gtk.h> +#include <unistd.h> +#include "mozilla/Sprintf.h" +#include "progressui.h" +#include "readstrings.h" +#include "errors.h" + +#define TIMER_INTERVAL 100 + +static float sProgressVal; // between 0 and 100 +static gboolean sQuit = FALSE; +static gboolean sEnableUI; +static guint sTimerID; + +static GtkWidget *sWin; +static GtkWidget *sLabel; +static GtkWidget *sProgressBar; + +static const char *sProgramPath; + +static gboolean +UpdateDialog(gpointer data) +{ + if (sQuit) + { + gtk_widget_hide(sWin); + gtk_main_quit(); + } + + float progress = sProgressVal; + + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(sProgressBar), + progress / 100.0); + + return TRUE; +} + +static gboolean +OnDeleteEvent(GtkWidget *widget, GdkEvent *event, gpointer user_data) +{ + return TRUE; +} + +int +InitProgressUI(int *pargc, char ***pargv) +{ + sProgramPath = (*pargv)[0]; + + sEnableUI = gtk_init_check(pargc, pargv); + return 0; +} + +int +ShowProgressUI() +{ + if (!sEnableUI) + return -1; + + // Only show the Progress UI if the process is taking a significant amount of + // time where a significant amount of time is defined as .5 seconds after + // ShowProgressUI is called sProgress is less than 70. + usleep(500000); + + if (sQuit || sProgressVal > 70.0f) + return 0; + + char ini_path[PATH_MAX]; + SprintfLiteral(ini_path, "%s.ini", sProgramPath); + + StringTable strings; + if (ReadStrings(ini_path, &strings) != OK) + return -1; + + sWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); + if (!sWin) + return -1; + + static GdkPixbuf *pixbuf; + char icon_path[PATH_MAX]; + SprintfLiteral(icon_path, "%s.png", sProgramPath); + + g_signal_connect(G_OBJECT(sWin), "delete_event", + G_CALLBACK(OnDeleteEvent), nullptr); + + gtk_window_set_title(GTK_WINDOW(sWin), strings.title); + gtk_window_set_type_hint(GTK_WINDOW(sWin), GDK_WINDOW_TYPE_HINT_DIALOG); + gtk_window_set_position(GTK_WINDOW(sWin), GTK_WIN_POS_CENTER_ALWAYS); + gtk_window_set_resizable(GTK_WINDOW(sWin), FALSE); + gtk_window_set_decorated(GTK_WINDOW(sWin), TRUE); + gtk_window_set_deletable(GTK_WINDOW(sWin),FALSE); + pixbuf = gdk_pixbuf_new_from_file (icon_path, nullptr); + gtk_window_set_icon(GTK_WINDOW(sWin), pixbuf); + g_object_unref(pixbuf); + + GtkWidget *vbox = gtk_vbox_new(TRUE, 6); + sLabel = gtk_label_new(strings.info); + gtk_misc_set_alignment(GTK_MISC(sLabel), 0.0f, 0.0f); + sProgressBar = gtk_progress_bar_new(); + + gtk_box_pack_start(GTK_BOX(vbox), sLabel, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), sProgressBar, TRUE, TRUE, 0); + + sTimerID = g_timeout_add(TIMER_INTERVAL, UpdateDialog, nullptr); + + gtk_container_set_border_width(GTK_CONTAINER(sWin), 10); + gtk_container_add(GTK_CONTAINER(sWin), vbox); + gtk_widget_show_all(sWin); + + gtk_main(); + return 0; +} + +// Called on a background thread +void +QuitProgressUI() +{ + sQuit = TRUE; +} + +// Called on a background thread +void +UpdateProgressUI(float progress) +{ + sProgressVal = progress; // 32-bit writes are atomic +} diff --git a/toolkit/mozapps/update/updater/progressui_null.cpp b/toolkit/mozapps/update/updater/progressui_null.cpp new file mode 100644 index 000000000..cb3ac6369 --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_null.cpp @@ -0,0 +1,25 @@ +/* -*- 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 "progressui.h" + +int InitProgressUI(int *argc, char ***argv) +{ + return 0; +} + +int ShowProgressUI() +{ + return 0; +} + +void QuitProgressUI() +{ +} + +void UpdateProgressUI(float progress) +{ +} diff --git a/toolkit/mozapps/update/updater/progressui_osx.mm b/toolkit/mozapps/update/updater/progressui_osx.mm new file mode 100644 index 000000000..54c9c41b7 --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_osx.mm @@ -0,0 +1,144 @@ +/* -*- 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/. */ + +#import <Cocoa/Cocoa.h> +#include <stdio.h> +#include <unistd.h> +#include "mozilla/Sprintf.h" +#include "progressui.h" +#include "readstrings.h" +#include "errors.h" + +#define TIMER_INTERVAL 0.2 + +static float sProgressVal; // between 0 and 100 +static BOOL sQuit = NO; +static BOOL sIndeterminate = NO; +static StringTable sLabels; +static const char *sUpdatePath; + +@interface UpdaterUI : NSObject +{ + IBOutlet NSProgressIndicator *progressBar; + IBOutlet NSTextField *progressTextField; +} +@end + +@implementation UpdaterUI + +-(void)awakeFromNib +{ + NSWindow *w = [progressBar window]; + + [w setTitle:[NSString stringWithUTF8String:sLabels.title]]; + [progressTextField setStringValue:[NSString stringWithUTF8String:sLabels.info]]; + + NSRect origTextFrame = [progressTextField frame]; + [progressTextField sizeToFit]; + + int widthAdjust = progressTextField.frame.size.width - origTextFrame.size.width; + + if (widthAdjust > 0) { + NSRect f; + f.size.width = w.frame.size.width + widthAdjust; + f.size.height = w.frame.size.height; + [w setFrame:f display:YES]; + } + + [w center]; + + [progressBar setIndeterminate:sIndeterminate]; + [progressBar setDoubleValue:0.0]; + + [[NSTimer scheduledTimerWithTimeInterval:TIMER_INTERVAL target:self + selector:@selector(updateProgressUI:) + userInfo:nil repeats:YES] retain]; + + // Make sure we are on top initially + [NSApp activateIgnoringOtherApps:YES]; +} + +// called when the timer goes off +-(void)updateProgressUI:(NSTimer *)aTimer +{ + if (sQuit) { + [aTimer invalidate]; + [aTimer release]; + + // It seems to be necessary to activate and hide ourselves before we stop, + // otherwise the "run" method will not return until the user focuses some + // other app. The activate step is necessary if we are not the active app. + // This is a big hack, but it seems to do the trick. + [NSApp activateIgnoringOtherApps:YES]; + [NSApp hide:self]; + [NSApp stop:self]; + } + + float progress = sProgressVal; + + [progressBar setDoubleValue:(double)progress]; +} + +// leave this as returning a BOOL instead of NSApplicationTerminateReply +// for backward compatibility +- (BOOL)applicationShouldTerminate:(NSApplication *)sender +{ + return sQuit; +} + +@end + +int +InitProgressUI(int *pargc, char ***pargv) +{ + sUpdatePath = (*pargv)[1]; + + return 0; +} + +int +ShowProgressUI(bool indeterminate) +{ + // Only show the Progress UI if the process is taking a significant amount of + // time where a significant amount of time is defined as .5 seconds after + // ShowProgressUI is called sProgress is less than 70. + usleep(500000); + + if (sQuit || sProgressVal > 70.0f) + return 0; + + char path[PATH_MAX]; + SprintfLiteral(path, "%s/updater.ini", sUpdatePath); + if (ReadStrings(path, &sLabels) != OK) + return -1; + + // Continue the update without showing the Progress UI if any of the supplied + // strings are larger than MAX_TEXT_LEN (Bug 628829). + if (!(strlen(sLabels.title) < MAX_TEXT_LEN - 1 && + strlen(sLabels.info) < MAX_TEXT_LEN - 1)) + return -1; + + sIndeterminate = indeterminate; + [NSApplication sharedApplication]; + [NSBundle loadNibNamed:@"MainMenu" owner:NSApp]; + [NSApp run]; + + return 0; +} + +// Called on a background thread +void +QuitProgressUI() +{ + sQuit = YES; +} + +// Called on a background thread +void +UpdateProgressUI(float progress) +{ + sProgressVal = progress; // 32-bit writes are atomic +} diff --git a/toolkit/mozapps/update/updater/progressui_win.cpp b/toolkit/mozapps/update/updater/progressui_win.cpp new file mode 100644 index 000000000..89bd71e85 --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_win.cpp @@ -0,0 +1,319 @@ +/* -*- 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 <stdio.h> +#include <windows.h> +#include <commctrl.h> +#include <process.h> +#include <io.h> + +#include "resource.h" +#include "progressui.h" +#include "readstrings.h" +#include "errors.h" + +#define TIMER_ID 1 +#define TIMER_INTERVAL 100 + +#define RESIZE_WINDOW(hwnd, extrax, extray) \ + { \ + RECT windowSize; \ + GetWindowRect(hwnd, &windowSize); \ + SetWindowPos(hwnd, 0, 0, 0, windowSize.right - windowSize.left + extrax, \ + windowSize.bottom - windowSize.top + extray, \ + SWP_NOMOVE | SWP_NOZORDER); \ + } + +#define MOVE_WINDOW(hwnd, dx, dy) \ + { \ + RECT rc; \ + POINT pt; \ + GetWindowRect(hwnd, &rc); \ + pt.x = rc.left; \ + pt.y = rc.top; \ + ScreenToClient(GetParent(hwnd), &pt); \ + SetWindowPos(hwnd, 0, pt.x + dx, pt.y + dy, 0, 0, \ + SWP_NOSIZE | SWP_NOZORDER); \ + } + +static float sProgress; // between 0 and 100 +static BOOL sQuit = FALSE; +static BOOL sIndeterminate = FALSE; +static StringTable sUIStrings; + +static BOOL +GetStringsFile(WCHAR filename[MAX_PATH]) +{ + if (!GetModuleFileNameW(nullptr, filename, MAX_PATH)) + return FALSE; + + WCHAR *dot = wcsrchr(filename, '.'); + if (!dot || wcsicmp(dot + 1, L"exe")) + return FALSE; + + wcscpy(dot + 1, L"ini"); + return TRUE; +} + +static void +UpdateDialog(HWND hDlg) +{ + int pos = int(sProgress + 0.5f); + HWND hWndPro = GetDlgItem(hDlg, IDC_PROGRESS); + SendMessage(hWndPro, PBM_SETPOS, pos, 0L); +} + +// The code in this function is from MSDN: +// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/dialogboxes/usingdialogboxes.asp +static void +CenterDialog(HWND hDlg) +{ + RECT rc, rcOwner, rcDlg; + + // Get the owner window and dialog box rectangles. + HWND desktop = GetDesktopWindow(); + + GetWindowRect(desktop, &rcOwner); + GetWindowRect(hDlg, &rcDlg); + CopyRect(&rc, &rcOwner); + + // Offset the owner and dialog box rectangles so that + // right and bottom values represent the width and + // height, and then offset the owner again to discard + // space taken up by the dialog box. + + OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top); + OffsetRect(&rc, -rc.left, -rc.top); + OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom); + + // The new position is the sum of half the remaining + // space and the owner's original position. + + SetWindowPos(hDlg, + HWND_TOP, + rcOwner.left + (rc.right / 2), + rcOwner.top + (rc.bottom / 2), + 0, 0, // ignores size arguments + SWP_NOSIZE); +} + +static void +InitDialog(HWND hDlg) +{ + WCHAR szwTitle[MAX_TEXT_LEN]; + WCHAR szwInfo[MAX_TEXT_LEN]; + + MultiByteToWideChar(CP_UTF8, 0, sUIStrings.title, -1, szwTitle, + sizeof(szwTitle)/sizeof(szwTitle[0])); + MultiByteToWideChar(CP_UTF8, 0, sUIStrings.info, -1, szwInfo, + sizeof(szwInfo)/sizeof(szwInfo[0])); + + SetWindowTextW(hDlg, szwTitle); + SetWindowTextW(GetDlgItem(hDlg, IDC_INFO), szwInfo); + + // Set dialog icon + HICON hIcon = LoadIcon(GetModuleHandle(nullptr), + MAKEINTRESOURCE(IDI_DIALOG)); + if (hIcon) + SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM) hIcon); + + HWND hWndPro = GetDlgItem(hDlg, IDC_PROGRESS); + SendMessage(hWndPro, PBM_SETRANGE, 0, MAKELPARAM(0, 100)); + if (sIndeterminate) { + LONG_PTR val = GetWindowLongPtr(hWndPro, GWL_STYLE); + SetWindowLongPtr(hWndPro, GWL_STYLE, val|PBS_MARQUEE); + SendMessage(hWndPro,(UINT) PBM_SETMARQUEE,(WPARAM) TRUE,(LPARAM)50 ); + } + + // Resize the dialog to fit all of the text if necessary. + RECT infoSize, textSize; + HWND hWndInfo = GetDlgItem(hDlg, IDC_INFO); + + // Get the control's font for calculating the new size for the control + HDC hDCInfo = GetDC(hWndInfo); + HFONT hInfoFont, hOldFont = NULL; + hInfoFont = (HFONT)SendMessage(hWndInfo, WM_GETFONT, 0, 0); + + if (hInfoFont) + hOldFont = (HFONT)SelectObject(hDCInfo, hInfoFont); + + // Measure the space needed for the text on a single line. DT_CALCRECT means + // nothing is drawn. + if (DrawText(hDCInfo, szwInfo, -1, &textSize, + DT_CALCRECT | DT_NOCLIP | DT_SINGLELINE)) { + GetClientRect(hWndInfo, &infoSize); + SIZE extra; + // Calculate the additional space needed for the text by subtracting from + // the rectangle returned by DrawText the existing client rectangle's width + // and height. + extra.cx = (textSize.right - textSize.left) - \ + (infoSize.right - infoSize.left); + extra.cy = (textSize.bottom - textSize.top) - \ + (infoSize.bottom - infoSize.top); + if (extra.cx < 0) + extra.cx = 0; + if (extra.cy < 0) + extra.cy = 0; + if ((extra.cx > 0) || (extra.cy > 0)) { + RESIZE_WINDOW(hDlg, extra.cx, extra.cy); + RESIZE_WINDOW(hWndInfo, extra.cx, extra.cy); + RESIZE_WINDOW(hWndPro, extra.cx, 0); + MOVE_WINDOW(hWndPro, 0, extra.cy); + } + } + + if (hOldFont) + SelectObject(hDCInfo, hOldFont); + + ReleaseDC(hWndInfo, hDCInfo); + + CenterDialog(hDlg); // make dialog appear in the center of the screen + + SetTimer(hDlg, TIMER_ID, TIMER_INTERVAL, nullptr); +} + +// Message handler for update dialog. +static LRESULT CALLBACK +DialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_INITDIALOG: + InitDialog(hDlg); + return TRUE; + + case WM_TIMER: + if (sQuit) { + EndDialog(hDlg, 0); + } else { + UpdateDialog(hDlg); + } + return TRUE; + + case WM_COMMAND: + return TRUE; + } + return FALSE; +} + +int +InitProgressUI(int *argc, WCHAR ***argv) +{ + return 0; +} + +/** + * Initializes the progress UI strings + * + * @return 0 on success, -1 on error +*/ +int +InitProgressUIStrings() { + // If we do not have updater.ini, then we should not bother showing UI. + WCHAR filename[MAX_PATH]; + if (!GetStringsFile(filename)) { + return -1; + } + + if (_waccess(filename, 04)) { + return -1; + } + + // If the updater.ini doesn't have the required strings, then we should not + // bother showing UI. + if (ReadStrings(filename, &sUIStrings) != OK) { + return -1; + } + + return 0; +} + +int +ShowProgressUI(bool indeterminate, bool initUIStrings) +{ + sIndeterminate = indeterminate; + if (!indeterminate) { + // Only show the Progress UI if the process is taking a significant amount of + // time where a significant amount of time is defined as .5 seconds after + // ShowProgressUI is called sProgress is less than 70. + Sleep(500); + + if (sQuit || sProgress > 70.0f) + return 0; + } + + // Don't load the UI if there's an <exe_name>.Local directory for redirection. + WCHAR appPath[MAX_PATH + 1] = { L'\0' }; + if (!GetModuleFileNameW(nullptr, appPath, MAX_PATH)) { + return -1; + } + + if (wcslen(appPath) + wcslen(L".Local") >= MAX_PATH) { + return -1; + } + + wcscat(appPath, L".Local"); + + if (!_waccess(appPath, 04)) { + return -1; + } + + // Don't load the UI if the strings for the UI are not provided. + if (initUIStrings && InitProgressUIStrings() == -1) { + return -1; + } + + if (!GetModuleFileNameW(nullptr, appPath, MAX_PATH)) { + return -1; + } + + // Use an activation context that supports visual styles for the controls. + ACTCTXW actx = {0}; + actx.cbSize = sizeof(ACTCTXW); + actx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_HMODULE_VALID; + actx.hModule = GetModuleHandle(NULL); // Use the embedded manifest + // This is needed only for Win XP but doesn't cause a problem with other + // versions of Windows. + actx.lpSource = appPath; + actx.lpResourceName = MAKEINTRESOURCE(IDR_COMCTL32_MANIFEST); + + HANDLE hactx = INVALID_HANDLE_VALUE; + hactx = CreateActCtxW(&actx); + ULONG_PTR actxCookie = NULL; + if (hactx != INVALID_HANDLE_VALUE) { + // Push the specified activation context to the top of the activation stack. + ActivateActCtx(hactx, &actxCookie); + } + + INITCOMMONCONTROLSEX icc = { + sizeof(INITCOMMONCONTROLSEX), + ICC_PROGRESS_CLASS + }; + InitCommonControlsEx(&icc); + + DialogBox(GetModuleHandle(nullptr), + MAKEINTRESOURCE(IDD_DIALOG), nullptr, + (DLGPROC) DialogProc); + + if (hactx != INVALID_HANDLE_VALUE) { + // Deactivate the context now that the comctl32.dll is loaded. + DeactivateActCtx(0, actxCookie); + } + + return 0; +} + +void +QuitProgressUI() +{ + sQuit = TRUE; +} + +void +UpdateProgressUI(float progress) +{ + sProgress = progress; // 32-bit writes are atomic +} diff --git a/toolkit/mozapps/update/updater/release_primary.der b/toolkit/mozapps/update/updater/release_primary.der Binary files differnew file mode 100644 index 000000000..11417c35e --- /dev/null +++ b/toolkit/mozapps/update/updater/release_primary.der diff --git a/toolkit/mozapps/update/updater/release_secondary.der b/toolkit/mozapps/update/updater/release_secondary.der Binary files differnew file mode 100644 index 000000000..16a7ef6d9 --- /dev/null +++ b/toolkit/mozapps/update/updater/release_secondary.der diff --git a/toolkit/mozapps/update/updater/resource.h b/toolkit/mozapps/update/updater/resource.h new file mode 100644 index 000000000..3cfa4efda --- /dev/null +++ b/toolkit/mozapps/update/updater/resource.h @@ -0,0 +1,29 @@ +/* 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/. */ + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by updater.rc +// +#define IDD_DIALOG 101 +#define IDC_PROGRESS 1000 +#define IDC_INFO 1002 +#define IDI_DIALOG 1003 +#define TYPE_CERT 512 +#define IDR_PRIMARY_CERT 1004 +#define IDR_BACKUP_CERT 1005 +#define IDS_UPDATER_IDENTITY 1006 +#define IDR_XPCSHELL_CERT 1007 +#define IDR_COMCTL32_MANIFEST 17 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1008 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/toolkit/mozapps/update/updater/updater-common.build b/toolkit/mozapps/update/updater/updater-common.build new file mode 100644 index 000000000..02b7338bc --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-common.build @@ -0,0 +1,136 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +srcs = [ + 'archivereader.cpp', + 'bspatch.cpp', + 'updater.cpp', +] + +have_progressui = 0 + +if CONFIG['MOZ_VERIFY_MAR_SIGNATURE']: + USE_LIBS += [ + 'verifymar', + ] + +if CONFIG['OS_ARCH'] == 'WINNT': + have_progressui = 1 + srcs += [ + 'loaddlls.cpp', + 'progressui_win.cpp', + 'win_dirent.cpp', + ] + RCINCLUDE = '%supdater.rc' % updater_rel_path + DEFINES['UNICODE'] = True + DEFINES['_UNICODE'] = True + USE_STATIC_LIBS = True + + # Pick up nsWindowsRestart.cpp + LOCAL_INCLUDES += [ + '/toolkit/xre', + ] + USE_LIBS += [ + 'updatecommon-standalone', + ] + OS_LIBS += [ + 'comctl32', + 'ws2_32', + 'shell32', + 'shlwapi', + 'crypt32', + 'advapi32', + ] +elif CONFIG['OS_ARCH'] == 'Linux' and CONFIG['MOZ_VERIFY_MAR_SIGNATURE']: + USE_LIBS += [ + 'nss', + 'signmar', + 'updatecommon', + ] + OS_LIBS += CONFIG['NSPR_LIBS'] +else: + USE_LIBS += [ + 'updatecommon', + ] + +USE_LIBS += [ + 'mar', +] + +if CONFIG['MOZ_SYSTEM_BZ2']: + OS_LIBS += CONFIG['MOZ_BZ2_LIBS'] +else: + USE_LIBS += [ + 'bz2', + ] + +if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: + have_progressui = 1 + srcs += [ + 'progressui_gtk.cpp', + ] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa': + have_progressui = 1 + srcs += [ + 'launchchild_osx.mm', + 'progressui_osx.mm', + ] + OS_LIBS += [ + '-framework Cocoa', + '-framework Security', + '-framework SystemConfiguration', + ] + UNIFIED_SOURCES += [ + '/toolkit/xre/updaterfileutils_osx.mm', + ] + LOCAL_INCLUDES += [ + '/toolkit/xre', + ] +elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk': + have_progressui = 1 + srcs += [ + 'automounter_gonk.cpp', + 'progressui_gonk.cpp', + ] + DISABLE_STL_WRAPPING = True + OS_LIBS += [ + 'cutils', + 'sysutils', + ] + +if have_progressui == 0: + srcs += [ + 'progressui_null.cpp', + ] + +SOURCES += sorted(srcs) + +DEFINES['NS_NO_XPCOM'] = True +DISABLE_STL_WRAPPING = True +for var in ('MAR_CHANNEL_ID', 'MOZ_APP_VERSION'): + DEFINES[var] = '"%s"' % CONFIG[var] + +LOCAL_INCLUDES += [ + '/toolkit/mozapps/update/common', + '/xpcom/glue', +] + +DELAYLOAD_DLLS += [ + 'crypt32.dll', + 'comctl32.dll', + 'userenv.dll', + 'wsock32.dll', +] + +if CONFIG['_MSC_VER']: + WIN32_EXE_LDFLAGS += ['-ENTRY:wmainCRTStartup'] +elif CONFIG['OS_ARCH'] == 'WINNT': + WIN32_EXE_LDFLAGS += ['-municode'] + +if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: + CXXFLAGS += CONFIG['TK_CFLAGS'] + OS_LIBS += CONFIG['TK_LIBS'] diff --git a/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in new file mode 100644 index 000000000..01822b186 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in @@ -0,0 +1,41 @@ +# vim:set ts=8 sw=8 sts=8 noet: +# 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/. + +# For changes here, also consider ../Makefile.in + +XPCSHELLTESTROOT = $(topobjdir)/_tests/xpcshell/toolkit/mozapps/update/tests +MOCHITESTROOT = $(topobjdir)/_tests/testing/mochitest/chrome/toolkit/mozapps/update/tests + +include $(topsrcdir)/config/rules.mk + +ifndef MOZ_WINCONSOLE +ifdef MOZ_DEBUG +MOZ_WINCONSOLE = 1 +else +MOZ_WINCONSOLE = 0 +endif +endif + +ifdef COMPILE_ENVIRONMENT +tools:: +ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) + # Copy for xpcshell tests + $(NSINSTALL) -D $(XPCSHELLTESTROOT)/data/updater-xpcshell.app + rsync -a -C --exclude '*.in' $(srcdir)/../macbuild/Contents $(XPCSHELLTESTROOT)/data/updater-xpcshell.app + sed -e 's/%APP_NAME%/$(MOZ_APP_DISPLAYNAME)/' $(srcdir)/../macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in | \ + iconv -f UTF-8 -t UTF-16 > $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/Resources/English.lproj/InfoPlist.strings + $(NSINSTALL) -D $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/MacOS/updater-xpcshell + $(NSINSTALL) updater-xpcshell $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/MacOS + rm -Rf $(XPCSHELLTESTROOT)/data/updater.app + mv $(XPCSHELLTESTROOT)/data/updater-xpcshell.app $(XPCSHELLTESTROOT)/data/updater.app + mv $(XPCSHELLTESTROOT)/data/updater.app/Contents/MacOS/updater-xpcshell $(XPCSHELLTESTROOT)/data/updater.app/Contents/MacOS/org.mozilla.updater + + # Copy for mochitest chrome tests + rsync -a -C $(XPCSHELLTESTROOT)/data/updater.app $(MOCHITESTROOT)/data/ +else + cp $(PROGRAM) $(XPCSHELLTESTROOT)/data/updater$(BIN_SUFFIX) + cp $(PROGRAM) $(MOCHITESTROOT)/data/updater$(BIN_SUFFIX) +endif +endif # COMPILE_ENVIRONMENT diff --git a/toolkit/mozapps/update/updater/updater-xpcshell/moz.build b/toolkit/mozapps/update/updater/updater-xpcshell/moz.build new file mode 100644 index 000000000..710b7e1de --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-xpcshell/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Program('updater-xpcshell') + +updater_rel_path = '../' +DIST_INSTALL = False +DEFINES['TEST_UPDATER'] = True +include('../updater-common.build') + +CXXFLAGS += CONFIG['MOZ_BZ2_CFLAGS'] diff --git a/toolkit/mozapps/update/updater/updater.cpp b/toolkit/mozapps/update/updater/updater.cpp new file mode 100644 index 000000000..63a92c084 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.cpp @@ -0,0 +1,4454 @@ +/* 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/. */ + +/** + * Manifest Format + * --------------- + * + * contents = 1*( line ) + * line = method LWS *( param LWS ) CRLF + * CRLF = "\r\n" + * LWS = 1*( " " | "\t" ) + * + * Available methods for the manifest file: + * + * updatev2.manifest + * ----------------- + * method = "add" | "add-if" | "patch" | "patch-if" | "remove" | + * "rmdir" | "rmrfdir" | type + * + * 'type' is the update type (e.g. complete or partial) and when present MUST + * be the first entry in the update manifest. The type is used to support + * downgrades by causing the actions defined in precomplete to be performed. + * + * updatev3.manifest + * ----------------- + * method = "add" | "add-if" | "add-if-not" | "patch" | "patch-if" | + * "remove" | "rmdir" | "rmrfdir" | type + * + * 'add-if-not' adds a file if it doesn't exist. + * + * precomplete + * ----------- + * method = "remove" | "rmdir" + */ +#include "bspatch.h" +#include "progressui.h" +#include "archivereader.h" +#include "readstrings.h" +#include "errors.h" +#include "bzlib.h" + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <stdarg.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <limits.h> +#include <errno.h> +#include <algorithm> + +#include "updatecommon.h" +#ifdef XP_MACOSX +#include "updaterfileutils_osx.h" +#endif // XP_MACOSX + +#include "mozilla/Compiler.h" +#include "mozilla/Types.h" +#include "mozilla/UniquePtr.h" + +// Amount of the progress bar to use in each of the 3 update stages, +// should total 100.0. +#define PROGRESS_PREPARE_SIZE 20.0f +#define PROGRESS_EXECUTE_SIZE 75.0f +#define PROGRESS_FINISH_SIZE 5.0f + +// Amount of time in ms to wait for the parent process to close +#ifdef DEBUG +// Use a large value for debug builds since the xpcshell tests take a long time. +#define PARENT_WAIT 30000 +#else +#define PARENT_WAIT 10000 +#endif + +#if defined(XP_MACOSX) +// These functions are defined in launchchild_osx.mm +void CleanupElevatedMacUpdate(bool aFailureOccurred); +bool IsOwnedByGroupAdmin(const char* aAppBundle); +bool IsRecursivelyWritable(const char* aPath); +void LaunchChild(int argc, const char** argv); +void LaunchMacPostProcess(const char* aAppBundle); +bool ObtainUpdaterArguments(int* argc, char*** argv); +bool ServeElevatedUpdate(int argc, const char** argv); +void SetGroupOwnershipAndPermissions(const char* aAppBundle); +struct UpdateServerThreadArgs +{ + int argc; + const NS_tchar** argv; +}; +#endif + +#ifndef _O_BINARY +# define _O_BINARY 0 +#endif + +#ifndef NULL +# define NULL (0) +#endif + +#ifndef SSIZE_MAX +# define SSIZE_MAX LONG_MAX +#endif + +// We want to use execv to invoke the callback executable on platforms where +// we were launched using execv. See nsUpdateDriver.cpp. +#if defined(XP_UNIX) && !defined(XP_MACOSX) +#define USE_EXECV +#endif + +#if defined(MOZ_WIDGET_GONK) +# include "automounter_gonk.h" +# include <unistd.h> +# include <android/log.h> +# include <linux/ioprio.h> +# include <sys/resource.h> + +#if ANDROID_VERSION < 21 +// The only header file in bionic which has a function prototype for ioprio_set +// is libc/include/sys/linux-unistd.h. However, linux-unistd.h conflicts +// badly with unistd.h, so we declare the prototype for ioprio_set directly. +extern "C" MOZ_EXPORT int ioprio_set(int which, int who, int ioprio); +#else +# include <sys/syscall.h> +static int ioprio_set(int which, int who, int ioprio) { + return syscall(__NR_ioprio_set, which, who, ioprio); +} +#endif + +# define MAYBE_USE_HARD_LINKS 1 +static bool sUseHardLinks = true; +#else +# define MAYBE_USE_HARD_LINKS 0 +#endif + +#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && \ + !defined(XP_MACOSX) && !defined(MOZ_WIDGET_GONK) +#include "nss.h" +#include "prerror.h" +#endif + +#ifdef XP_WIN +#ifdef MOZ_MAINTENANCE_SERVICE +#include "registrycertificates.h" +#endif +BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra); +BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, + LPCWSTR siblingFilePath, + LPCWSTR newFileName); +#include "updatehelper.h" + +// Closes the handle if valid and if the updater is elevated returns with the +// return code specified. This prevents multiple launches of the callback +// application by preventing the elevated process from launching the callback. +#define EXIT_WHEN_ELEVATED(path, handle, retCode) \ + { \ + if (handle != INVALID_HANDLE_VALUE) { \ + CloseHandle(handle); \ + } \ + if (_waccess(path, F_OK) == 0 && NS_tremove(path) != 0) { \ + LogFinish(); \ + return retCode; \ + } \ + } +#endif + +//----------------------------------------------------------------------------- + +// This variable lives in libbz2. It's declared in bzlib_private.h, so we just +// declare it here to avoid including that entire header file. +#define BZ2_CRC32TABLE_UNDECLARED + +#if MOZ_IS_GCC || defined(__clang__) +extern "C" __attribute__((visibility("default"))) unsigned int BZ2_crc32Table[256]; +#undef BZ2_CRC32TABLE_UNDECLARED +#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC) +extern "C" __global unsigned int BZ2_crc32Table[256]; +#undef BZ2_CRC32TABLE_UNDECLARED +#endif +#if defined(BZ2_CRC32TABLE_UNDECLARED) +extern "C" unsigned int BZ2_crc32Table[256]; +#undef BZ2_CRC32TABLE_UNDECLARED +#endif + +static unsigned int +crc32(const unsigned char *buf, unsigned int len) +{ + unsigned int crc = 0xffffffffL; + + const unsigned char *end = buf + len; + for (; buf != end; ++buf) + crc = (crc << 8) ^ BZ2_crc32Table[(crc >> 24) ^ *buf]; + + crc = ~crc; + return crc; +} + +//----------------------------------------------------------------------------- + +// A simple stack based container for a FILE struct that closes the +// file descriptor from its destructor. +class AutoFile +{ +public: + explicit AutoFile(FILE* file = nullptr) + : mFile(file) { + } + + ~AutoFile() { + if (mFile != nullptr) + fclose(mFile); + } + + AutoFile &operator=(FILE* file) { + if (mFile != 0) + fclose(mFile); + mFile = file; + return *this; + } + + operator FILE*() { + return mFile; + } + + FILE* get() { + return mFile; + } + +private: + FILE* mFile; +}; + +struct MARChannelStringTable { + MARChannelStringTable() + { + MARChannelID[0] = '\0'; + } + + char MARChannelID[MAX_TEXT_LEN]; +}; + +//----------------------------------------------------------------------------- + +typedef void (* ThreadFunc)(void *param); + +#ifdef XP_WIN +#include <process.h> + +class Thread +{ +public: + int Run(ThreadFunc func, void *param) + { + mThreadFunc = func; + mThreadParam = param; + + unsigned int threadID; + + mThread = (HANDLE) _beginthreadex(nullptr, 0, ThreadMain, this, 0, + &threadID); + + return mThread ? 0 : -1; + } + int Join() + { + WaitForSingleObject(mThread, INFINITE); + CloseHandle(mThread); + return 0; + } +private: + static unsigned __stdcall ThreadMain(void *p) + { + Thread *self = (Thread *) p; + self->mThreadFunc(self->mThreadParam); + return 0; + } + HANDLE mThread; + ThreadFunc mThreadFunc; + void *mThreadParam; +}; + +#elif defined(XP_UNIX) +#include <pthread.h> + +class Thread +{ +public: + int Run(ThreadFunc func, void *param) + { + return pthread_create(&thr, nullptr, (void* (*)(void *)) func, param); + } + int Join() + { + void *result; + return pthread_join(thr, &result); + } +private: + pthread_t thr; +}; + +#else +#error "Unsupported platform" +#endif + +//----------------------------------------------------------------------------- + +static NS_tchar gPatchDirPath[MAXPATHLEN]; +static NS_tchar gInstallDirPath[MAXPATHLEN]; +static NS_tchar gWorkingDirPath[MAXPATHLEN]; +static ArchiveReader gArchiveReader; +static bool gSucceeded = false; +static bool sStagedUpdate = false; +static bool sReplaceRequest = false; +static bool sUsingService = false; +static bool sIsOSUpdate = false; + +#ifdef XP_WIN +// The current working directory specified in the command line. +static NS_tchar* gDestPath; +static NS_tchar gCallbackRelPath[MAXPATHLEN]; +static NS_tchar gCallbackBackupPath[MAXPATHLEN]; +static NS_tchar gDeleteDirPath[MAXPATHLEN]; +#endif + +static const NS_tchar kWhitespace[] = NS_T(" \t"); +static const NS_tchar kNL[] = NS_T("\r\n"); +static const NS_tchar kQuote[] = NS_T("\""); + +static inline size_t +mmin(size_t a, size_t b) +{ + return (a > b) ? b : a; +} + +static NS_tchar* +mstrtok(const NS_tchar *delims, NS_tchar **str) +{ + if (!*str || !**str) { + *str = nullptr; + return nullptr; + } + + // skip leading "whitespace" + NS_tchar *ret = *str; + const NS_tchar *d; + do { + for (d = delims; *d != NS_T('\0'); ++d) { + if (*ret == *d) { + ++ret; + break; + } + } + } while (*d); + + if (!*ret) { + *str = ret; + return nullptr; + } + + NS_tchar *i = ret; + do { + for (d = delims; *d != NS_T('\0'); ++d) { + if (*i == *d) { + *i = NS_T('\0'); + *str = ++i; + return ret; + } + } + ++i; + } while (*i); + + *str = nullptr; + return ret; +} + +static bool +EnvHasValue(const char *name) +{ + const char *val = getenv(name); + return (val && *val); +} + +/** + * Coverts a relative update path to a full path. + * + * @param relpath + * The relative path to convert to a full path. + * @return valid filesystem full path or nullptr if memory allocation fails. + */ +static NS_tchar* +get_full_path(const NS_tchar *relpath) +{ + NS_tchar *destpath = sStagedUpdate ? gWorkingDirPath : gInstallDirPath; + size_t lendestpath = NS_tstrlen(destpath); + size_t lenrelpath = NS_tstrlen(relpath); + NS_tchar *s = new NS_tchar[lendestpath + lenrelpath + 2]; + if (!s) { + return nullptr; + } + + NS_tchar *c = s; + + NS_tstrcpy(c, destpath); + c += lendestpath; + NS_tstrcat(c, NS_T("/")); + c++; + + NS_tstrcat(c, relpath); + c += lenrelpath; + *c = NS_T('\0'); + return s; +} + +/** + * Converts a full update path into a relative path; reverses get_full_path. + * + * @param fullpath + * The absolute path to convert into a relative path. + * return pointer to the location within fullpath where the relative path starts + * or fullpath itself if it already looks relative. + */ +static const NS_tchar* +get_relative_path(const NS_tchar *fullpath) +{ + // If the path isn't absolute, just return it as-is. +#ifdef XP_WIN + if (fullpath[1] != ':' && fullpath[2] != '\\') { +#else + if (fullpath[0] != '/') { +#endif + return fullpath; + } + + NS_tchar *prefix = sStagedUpdate ? gWorkingDirPath : gInstallDirPath; + + // If the path isn't long enough to be absolute, return it as-is. + if (NS_tstrlen(fullpath) <= NS_tstrlen(prefix)) { + return fullpath; + } + + return fullpath + NS_tstrlen(prefix) + 1; +} + +/** + * Gets the platform specific path and performs simple checks to the path. If + * the path checks don't pass nullptr will be returned. + * + * @param line + * The line from the manifest that contains the path. + * @param isdir + * Whether the path is a directory path. Defaults to false. + * @return valid filesystem path or nullptr if the path checks fail. + */ +static NS_tchar* +get_valid_path(NS_tchar **line, bool isdir = false) +{ + NS_tchar *path = mstrtok(kQuote, line); + if (!path) { + LOG(("get_valid_path: unable to determine path: " LOG_S, line)); + return nullptr; + } + + // All paths must be relative from the current working directory + if (path[0] == NS_T('/')) { + LOG(("get_valid_path: path must be relative: " LOG_S, path)); + return nullptr; + } + +#ifdef XP_WIN + // All paths must be relative from the current working directory + if (path[0] == NS_T('\\') || path[1] == NS_T(':')) { + LOG(("get_valid_path: path must be relative: " LOG_S, path)); + return nullptr; + } +#endif + + if (isdir) { + // Directory paths must have a trailing forward slash. + if (path[NS_tstrlen(path) - 1] != NS_T('/')) { + LOG(("get_valid_path: directory paths must have a trailing forward " \ + "slash: " LOG_S, path)); + return nullptr; + } + + // Remove the trailing forward slash because stat on Windows will return + // ENOENT if the path has a trailing slash. + path[NS_tstrlen(path) - 1] = NS_T('\0'); + } + + // Don't allow relative paths that resolve to a parent directory. + if (NS_tstrstr(path, NS_T("..")) != nullptr) { + LOG(("get_valid_path: paths must not contain '..': " LOG_S, path)); + return nullptr; + } + + return path; +} + +static NS_tchar* +get_quoted_path(const NS_tchar *path) +{ + size_t lenQuote = NS_tstrlen(kQuote); + size_t lenPath = NS_tstrlen(path); + size_t len = lenQuote + lenPath + lenQuote + 1; + + NS_tchar *s = (NS_tchar *) malloc(len * sizeof(NS_tchar)); + if (!s) + return nullptr; + + NS_tchar *c = s; + NS_tstrcpy(c, kQuote); + c += lenQuote; + NS_tstrcat(c, path); + c += lenPath; + NS_tstrcat(c, kQuote); + c += lenQuote; + *c = NS_T('\0'); + c++; + return s; +} + +static void ensure_write_permissions(const NS_tchar *path) +{ +#ifdef XP_WIN + (void) _wchmod(path, _S_IREAD | _S_IWRITE); +#else + struct stat fs; + if (!stat(path, &fs) && !(fs.st_mode & S_IWUSR)) { + (void)chmod(path, fs.st_mode | S_IWUSR); + } +#endif +} + +static int ensure_remove(const NS_tchar *path) +{ + ensure_write_permissions(path); + int rv = NS_tremove(path); + if (rv) + LOG(("ensure_remove: failed to remove file: " LOG_S ", rv: %d, err: %d", + path, rv, errno)); + return rv; +} + +// Remove the directory pointed to by path and all of its files and sub-directories. +static int ensure_remove_recursive(const NS_tchar *path, + bool continueEnumOnFailure = false) +{ + // We use lstat rather than stat here so that we can successfully remove + // symlinks. + struct NS_tstat_t sInfo; + int rv = NS_tlstat(path, &sInfo); + if (rv) { + // This error is benign + return rv; + } + if (!S_ISDIR(sInfo.st_mode)) { + return ensure_remove(path); + } + + NS_tDIR *dir; + NS_tdirent *entry; + + dir = NS_topendir(path); + if (!dir) { + LOG(("ensure_remove_recursive: unable to open directory: " LOG_S + ", rv: %d, err: %d", path, rv, errno)); + return rv; + } + + while ((entry = NS_treaddir(dir)) != 0) { + if (NS_tstrcmp(entry->d_name, NS_T(".")) && + NS_tstrcmp(entry->d_name, NS_T(".."))) { + NS_tchar childPath[MAXPATHLEN]; + NS_tsnprintf(childPath, sizeof(childPath)/sizeof(childPath[0]), + NS_T("%s/%s"), path, entry->d_name); + rv = ensure_remove_recursive(childPath); + if (rv && !continueEnumOnFailure) { + break; + } + } + } + + NS_tclosedir(dir); + + if (rv == OK) { + ensure_write_permissions(path); + rv = NS_trmdir(path); + if (rv) { + LOG(("ensure_remove_recursive: unable to remove directory: " LOG_S + ", rv: %d, err: %d", path, rv, errno)); + } + } + return rv; +} + +static bool is_read_only(const NS_tchar *flags) +{ + size_t length = NS_tstrlen(flags); + if (length == 0) + return false; + + // Make sure the string begins with "r" + if (flags[0] != NS_T('r')) + return false; + + // Look for "r+" or "r+b" + if (length > 1 && flags[1] == NS_T('+')) + return false; + + // Look for "rb+" + if (NS_tstrcmp(flags, NS_T("rb+")) == 0) + return false; + + return true; +} + +static FILE* ensure_open(const NS_tchar *path, const NS_tchar *flags, unsigned int options) +{ + ensure_write_permissions(path); + FILE* f = NS_tfopen(path, flags); + if (is_read_only(flags)) { + // Don't attempt to modify the file permissions if the file is being opened + // in read-only mode. + return f; + } + if (NS_tchmod(path, options) != 0) { + if (f != nullptr) { + fclose(f); + } + return nullptr; + } + struct NS_tstat_t ss; + if (NS_tstat(path, &ss) != 0 || ss.st_mode != options) { + if (f != nullptr) { + fclose(f); + } + return nullptr; + } + return f; +} + +// Ensure that the directory containing this file exists. +static int ensure_parent_dir(const NS_tchar *path) +{ + int rv = OK; + + NS_tchar *slash = (NS_tchar *) NS_tstrrchr(path, NS_T('/')); + if (slash) { + *slash = NS_T('\0'); + rv = ensure_parent_dir(path); + // Only attempt to create the directory if we're not at the root + if (rv == OK && *path) { + rv = NS_tmkdir(path, 0755); + // If the directory already exists, then ignore the error. + if (rv < 0 && errno != EEXIST) { + LOG(("ensure_parent_dir: failed to create directory: " LOG_S ", " \ + "err: %d", path, errno)); + rv = WRITE_ERROR; + } else { + rv = OK; + } + } + *slash = NS_T('/'); + } + return rv; +} + +#ifdef XP_UNIX +static int ensure_copy_symlink(const NS_tchar *path, const NS_tchar *dest) +{ + // Copy symlinks by creating a new symlink to the same target + NS_tchar target[MAXPATHLEN + 1] = {NS_T('\0')}; + int rv = readlink(path, target, MAXPATHLEN); + if (rv == -1) { + LOG(("ensure_copy_symlink: failed to read the link: " LOG_S ", err: %d", + path, errno)); + return READ_ERROR; + } + rv = symlink(target, dest); + if (rv == -1) { + LOG(("ensure_copy_symlink: failed to create the new link: " LOG_S ", target: " LOG_S " err: %d", + dest, target, errno)); + return READ_ERROR; + } + return 0; +} +#endif + +#if MAYBE_USE_HARD_LINKS +/* + * Creates a hardlink (destFilename) which points to the existing file + * (srcFilename). + * + * @return 0 if successful, an error otherwise + */ + +static int +create_hard_link(const NS_tchar *srcFilename, const NS_tchar *destFilename) +{ + if (link(srcFilename, destFilename) < 0) { + LOG(("link(%s, %s) failed errno = %d", srcFilename, destFilename, errno)); + return WRITE_ERROR; + } + return OK; +} +#endif + +// Copy the file named path onto a new file named dest. +static int ensure_copy(const NS_tchar *path, const NS_tchar *dest) +{ +#ifdef XP_WIN + // Fast path for Windows + bool result = CopyFileW(path, dest, false); + if (!result) { + LOG(("ensure_copy: failed to copy the file " LOG_S " over to " LOG_S ", lasterr: %x", + path, dest, GetLastError())); + return WRITE_ERROR_FILE_COPY; + } + return OK; +#else + struct NS_tstat_t ss; + int rv = NS_tlstat(path, &ss); + if (rv) { + LOG(("ensure_copy: failed to read file status info: " LOG_S ", err: %d", + path, errno)); + return READ_ERROR; + } + +#ifdef XP_UNIX + if (S_ISLNK(ss.st_mode)) { + return ensure_copy_symlink(path, dest); + } +#endif + +#if MAYBE_USE_HARD_LINKS + if (sUseHardLinks) { + if (!create_hard_link(path, dest)) { + return OK; + } + // Since we failed to create the hard link, fall through and copy the file. + sUseHardLinks = false; + } +#endif + + AutoFile infile(ensure_open(path, NS_T("rb"), ss.st_mode)); + if (!infile) { + LOG(("ensure_copy: failed to open the file for reading: " LOG_S ", err: %d", + path, errno)); + return READ_ERROR; + } + AutoFile outfile(ensure_open(dest, NS_T("wb"), ss.st_mode)); + if (!outfile) { + LOG(("ensure_copy: failed to open the file for writing: " LOG_S ", err: %d", + dest, errno)); + return WRITE_ERROR; + } + + // This block size was chosen pretty arbitrarily but seems like a reasonable + // compromise. For example, the optimal block size on a modern OS X machine + // is 100k */ + const int blockSize = 32 * 1024; + void* buffer = malloc(blockSize); + if (!buffer) + return UPDATER_MEM_ERROR; + + while (!feof(infile.get())) { + size_t read = fread(buffer, 1, blockSize, infile); + if (ferror(infile.get())) { + LOG(("ensure_copy: failed to read the file: " LOG_S ", err: %d", + path, errno)); + free(buffer); + return READ_ERROR; + } + + size_t written = 0; + + while (written < read) { + size_t chunkWritten = fwrite(buffer, 1, read - written, outfile); + if (chunkWritten <= 0) { + LOG(("ensure_copy: failed to write the file: " LOG_S ", err: %d", + dest, errno)); + free(buffer); + return WRITE_ERROR_FILE_COPY; + } + + written += chunkWritten; + } + } + + rv = NS_tchmod(dest, ss.st_mode); + + free(buffer); + return rv; +#endif +} + +template <unsigned N> +struct copy_recursive_skiplist { + NS_tchar paths[N][MAXPATHLEN]; + + void append(unsigned index, const NS_tchar *path, const NS_tchar *suffix) { + NS_tsnprintf(paths[index], MAXPATHLEN, NS_T("%s/%s"), path, suffix); + } + + bool find(const NS_tchar *path) { + for (int i = 0; i < static_cast<int>(N); ++i) { + if (!NS_tstricmp(paths[i], path)) { + return true; + } + } + return false; + } +}; + +// Copy all of the files and subdirectories under path to a new directory named dest. +// The path names in the skiplist will be skipped and will not be copied. +template <unsigned N> +static int ensure_copy_recursive(const NS_tchar *path, const NS_tchar *dest, + copy_recursive_skiplist<N>& skiplist) +{ + struct NS_tstat_t sInfo; + int rv = NS_tlstat(path, &sInfo); + if (rv) { + LOG(("ensure_copy_recursive: path doesn't exist: " LOG_S ", rv: %d, err: %d", + path, rv, errno)); + return READ_ERROR; + } + +#ifdef XP_UNIX + if (S_ISLNK(sInfo.st_mode)) { + return ensure_copy_symlink(path, dest); + } +#endif + + if (!S_ISDIR(sInfo.st_mode)) { + return ensure_copy(path, dest); + } + + rv = NS_tmkdir(dest, sInfo.st_mode); + if (rv < 0 && errno != EEXIST) { + LOG(("ensure_copy_recursive: could not create destination directory: " LOG_S ", rv: %d, err: %d", + path, rv, errno)); + return WRITE_ERROR; + } + + NS_tDIR *dir; + NS_tdirent *entry; + + dir = NS_topendir(path); + if (!dir) { + LOG(("ensure_copy_recursive: path is not a directory: " LOG_S ", rv: %d, err: %d", + path, rv, errno)); + return READ_ERROR; + } + + while ((entry = NS_treaddir(dir)) != 0) { + if (NS_tstrcmp(entry->d_name, NS_T(".")) && + NS_tstrcmp(entry->d_name, NS_T(".."))) { + NS_tchar childPath[MAXPATHLEN]; + NS_tsnprintf(childPath, sizeof(childPath)/sizeof(childPath[0]), + NS_T("%s/%s"), path, entry->d_name); + if (skiplist.find(childPath)) { + continue; + } + NS_tchar childPathDest[MAXPATHLEN]; + NS_tsnprintf(childPathDest, sizeof(childPathDest)/sizeof(childPathDest[0]), + NS_T("%s/%s"), dest, entry->d_name); + rv = ensure_copy_recursive(childPath, childPathDest, skiplist); + if (rv) { + break; + } + } + } + NS_tclosedir(dir); + return rv; +} + +// Renames the specified file to the new file specified. If the destination file +// exists it is removed. +static int rename_file(const NS_tchar *spath, const NS_tchar *dpath, + bool allowDirs = false) +{ + int rv = ensure_parent_dir(dpath); + if (rv) + return rv; + + struct NS_tstat_t spathInfo; + rv = NS_tstat(spath, &spathInfo); + if (rv) { + LOG(("rename_file: failed to read file status info: " LOG_S ", " \ + "err: %d", spath, errno)); + return READ_ERROR; + } + + if (!S_ISREG(spathInfo.st_mode)) { + if (allowDirs && !S_ISDIR(spathInfo.st_mode)) { + LOG(("rename_file: path present, but not a file: " LOG_S ", err: %d", + spath, errno)); + return RENAME_ERROR_EXPECTED_FILE; + } else { + LOG(("rename_file: proceeding to rename the directory")); + } + } + + if (!NS_taccess(dpath, F_OK)) { + if (ensure_remove(dpath)) { + LOG(("rename_file: destination file exists and could not be " \ + "removed: " LOG_S, dpath)); + return WRITE_ERROR_DELETE_FILE; + } + } + + if (NS_trename(spath, dpath) != 0) { + LOG(("rename_file: failed to rename file - src: " LOG_S ", " \ + "dst:" LOG_S ", err: %d", spath, dpath, errno)); + return WRITE_ERROR; + } + + return OK; +} + +#ifdef XP_WIN +// Remove the directory pointed to by path and all of its files and +// sub-directories. If a file is in use move it to the tobedeleted directory +// and attempt to schedule removal of the file on reboot +static int remove_recursive_on_reboot(const NS_tchar *path, const NS_tchar *deleteDir) +{ + struct NS_tstat_t sInfo; + int rv = NS_tlstat(path, &sInfo); + if (rv) { + // This error is benign + return rv; + } + + if (!S_ISDIR(sInfo.st_mode)) { + NS_tchar tmpDeleteFile[MAXPATHLEN]; + GetTempFileNameW(deleteDir, L"rep", 0, tmpDeleteFile); + NS_tremove(tmpDeleteFile); + rv = rename_file(path, tmpDeleteFile, false); + if (MoveFileEx(rv ? path : tmpDeleteFile, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG(("remove_recursive_on_reboot: file will be removed on OS reboot: " + LOG_S, rv ? path : tmpDeleteFile)); + } else { + LOG(("remove_recursive_on_reboot: failed to schedule OS reboot removal of " + "file: " LOG_S, rv ? path : tmpDeleteFile)); + } + return rv; + } + + NS_tDIR *dir; + NS_tdirent *entry; + + dir = NS_topendir(path); + if (!dir) { + LOG(("remove_recursive_on_reboot: unable to open directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return rv; + } + + while ((entry = NS_treaddir(dir)) != 0) { + if (NS_tstrcmp(entry->d_name, NS_T(".")) && + NS_tstrcmp(entry->d_name, NS_T(".."))) { + NS_tchar childPath[MAXPATHLEN]; + NS_tsnprintf(childPath, sizeof(childPath)/sizeof(childPath[0]), + NS_T("%s/%s"), path, entry->d_name); + // There is no need to check the return value of this call since this + // function is only called after an update is successful and there is not + // much that can be done to recover if it isn't successful. There is also + // no need to log the value since it will have already been logged. + remove_recursive_on_reboot(childPath, deleteDir); + } + } + + NS_tclosedir(dir); + + if (rv == OK) { + ensure_write_permissions(path); + rv = NS_trmdir(path); + if (rv) { + LOG(("remove_recursive_on_reboot: unable to remove directory: " LOG_S + ", rv: %d, err: %d", path, rv, errno)); + } + } + return rv; +} +#endif + +//----------------------------------------------------------------------------- + +// Create a backup of the specified file by renaming it. +static int backup_create(const NS_tchar *path) +{ + NS_tchar backup[MAXPATHLEN]; + NS_tsnprintf(backup, sizeof(backup)/sizeof(backup[0]), + NS_T("%s") BACKUP_EXT, path); + + return rename_file(path, backup); +} + +// Rename the backup of the specified file that was created by renaming it back +// to the original file. +static int backup_restore(const NS_tchar *path, const NS_tchar *relPath) +{ + NS_tchar backup[MAXPATHLEN]; + NS_tsnprintf(backup, sizeof(backup)/sizeof(backup[0]), + NS_T("%s") BACKUP_EXT, path); + + NS_tchar relBackup[MAXPATHLEN]; + NS_tsnprintf(relBackup, sizeof(relBackup) / sizeof(relBackup[0]), + NS_T("%s") BACKUP_EXT, relPath); + + if (NS_taccess(backup, F_OK)) { + LOG(("backup_restore: backup file doesn't exist: " LOG_S, relBackup)); + return OK; + } + + return rename_file(backup, path); +} + +// Discard the backup of the specified file that was created by renaming it. +static int backup_discard(const NS_tchar *path, const NS_tchar *relPath) +{ + NS_tchar backup[MAXPATHLEN]; + NS_tsnprintf(backup, sizeof(backup)/sizeof(backup[0]), + NS_T("%s") BACKUP_EXT, path); + + NS_tchar relBackup[MAXPATHLEN]; + NS_tsnprintf(relBackup, sizeof(relBackup) / sizeof(relBackup[0]), + NS_T("%s") BACKUP_EXT, relPath); + + // Nothing to discard + if (NS_taccess(backup, F_OK)) { + return OK; + } + + int rv = ensure_remove(backup); +#if defined(XP_WIN) + if (rv && !sStagedUpdate && !sReplaceRequest) { + LOG(("backup_discard: unable to remove: " LOG_S, relBackup)); + NS_tchar path[MAXPATHLEN]; + GetTempFileNameW(gDeleteDirPath, L"moz", 0, path); + if (rename_file(backup, path)) { + LOG(("backup_discard: failed to rename file:" LOG_S ", dst:" LOG_S, + relBackup, relPath)); + return WRITE_ERROR_DELETE_BACKUP; + } + // The MoveFileEx call to remove the file on OS reboot will fail if the + // process doesn't have write access to the HKEY_LOCAL_MACHINE registry key + // but this is ok since the installer / uninstaller will delete the + // directory containing the file along with its contents after an update is + // applied, on reinstall, and on uninstall. + if (MoveFileEx(path, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG(("backup_discard: file renamed and will be removed on OS " \ + "reboot: " LOG_S, relPath)); + } else { + LOG(("backup_discard: failed to schedule OS reboot removal of " \ + "file: " LOG_S, relPath)); + } + } +#else + if (rv) + return WRITE_ERROR_DELETE_BACKUP; +#endif + + return OK; +} + +// Helper function for post-processing a temporary backup. +static void backup_finish(const NS_tchar *path, const NS_tchar *relPath, + int status) +{ + if (status == OK) + backup_discard(path, relPath); + else + backup_restore(path, relPath); +} + +//----------------------------------------------------------------------------- + +static int DoUpdate(); + +class Action +{ +public: + Action() : mProgressCost(1), mNext(nullptr) { } + virtual ~Action() { } + + virtual int Parse(NS_tchar *line) = 0; + + // Do any preprocessing to ensure that the action can be performed. Execute + // will be called if this Action and all others return OK from this method. + virtual int Prepare() = 0; + + // Perform the operation. Return OK to indicate success. After all actions + // have been executed, Finish will be called. A requirement of Execute is + // that its operation be reversable from Finish. + virtual int Execute() = 0; + + // Finish is called after execution of all actions. If status is OK, then + // all actions were successfully executed. Otherwise, some action failed. + virtual void Finish(int status) = 0; + + int mProgressCost; +private: + Action* mNext; + + friend class ActionList; +}; + +class RemoveFile : public Action +{ +public: + RemoveFile() : mSkip(0) { } + + int Parse(NS_tchar *line); + int Prepare(); + int Execute(); + void Finish(int status); + +private: + mozilla::UniquePtr<NS_tchar[]> mFile; + mozilla::UniquePtr<NS_tchar[]> mRelPath; + int mSkip; +}; + +int +RemoveFile::Parse(NS_tchar *line) +{ + // format "<deadfile>" + + NS_tchar * validPath = get_valid_path(&line); + if (!validPath) { + return PARSE_ERROR; + } + + mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mRelPath.get(), validPath); + + mFile.reset(get_full_path(validPath)); + if (!mFile) { + return PARSE_ERROR; + } + + return OK; +} + +int +RemoveFile::Prepare() +{ + // Skip the file if it already doesn't exist. + int rv = NS_taccess(mFile.get(), F_OK); + if (rv) { + mSkip = 1; + mProgressCost = 0; + return OK; + } + + LOG(("PREPARE REMOVEFILE " LOG_S, mRelPath.get())); + + // Make sure that we're actually a file... + struct NS_tstat_t fileInfo; + rv = NS_tstat(mFile.get(), &fileInfo); + if (rv) { + LOG(("failed to read file status info: " LOG_S ", err: %d", mFile.get(), + errno)); + return READ_ERROR; + } + + if (!S_ISREG(fileInfo.st_mode)) { + LOG(("path present, but not a file: " LOG_S, mFile.get())); + return DELETE_ERROR_EXPECTED_FILE; + } + + NS_tchar *slash = (NS_tchar *) NS_tstrrchr(mFile.get(), NS_T('/')); + if (slash) { + *slash = NS_T('\0'); + rv = NS_taccess(mFile.get(), W_OK); + *slash = NS_T('/'); + } else { + rv = NS_taccess(NS_T("."), W_OK); + } + + if (rv) { + LOG(("access failed: %d", errno)); + return WRITE_ERROR_FILE_ACCESS_DENIED; + } + + return OK; +} + +int +RemoveFile::Execute() +{ + if (mSkip) + return OK; + + LOG(("EXECUTE REMOVEFILE " LOG_S, mRelPath.get())); + + // The file is checked for existence here and in Prepare since it might have + // been removed by a separate instruction: bug 311099. + int rv = NS_taccess(mFile.get(), F_OK); + if (rv) { + LOG(("file cannot be removed because it does not exist; skipping")); + mSkip = 1; + return OK; + } + + if (sStagedUpdate) { + // Staged updates don't need backup files so just remove it. + rv = ensure_remove(mFile.get()); + if (rv) { + return rv; + } + } else { + // Rename the old file. It will be removed in Finish. + rv = backup_create(mFile.get()); + if (rv) { + LOG(("backup_create failed: %d", rv)); + return rv; + } + } + + return OK; +} + +void +RemoveFile::Finish(int status) +{ + if (mSkip) { + return; + } + + LOG(("FINISH REMOVEFILE " LOG_S, mRelPath.get())); + + // Staged updates don't create backup files. + if (!sStagedUpdate) { + backup_finish(mFile.get(), mRelPath.get(), status); + } +} + +class RemoveDir : public Action +{ +public: + RemoveDir() : mSkip(0) { } + + virtual int Parse(NS_tchar *line); + virtual int Prepare(); // check that the source dir exists + virtual int Execute(); + virtual void Finish(int status); + +private: + mozilla::UniquePtr<NS_tchar[]> mDir; + mozilla::UniquePtr<NS_tchar[]> mRelPath; + int mSkip; +}; + +int +RemoveDir::Parse(NS_tchar *line) +{ + // format "<deaddir>/" + + NS_tchar * validPath = get_valid_path(&line, true); + if (!validPath) { + return PARSE_ERROR; + } + + mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mRelPath.get(), validPath); + + mDir.reset(get_full_path(validPath)); + if (!mDir) { + return PARSE_ERROR; + } + + return OK; +} + +int +RemoveDir::Prepare() +{ + // We expect the directory to exist if we are to remove it. + int rv = NS_taccess(mDir.get(), F_OK); + if (rv) { + mSkip = 1; + mProgressCost = 0; + return OK; + } + + LOG(("PREPARE REMOVEDIR " LOG_S "/", mRelPath.get())); + + // Make sure that we're actually a dir. + struct NS_tstat_t dirInfo; + rv = NS_tstat(mDir.get(), &dirInfo); + if (rv) { + LOG(("failed to read directory status info: " LOG_S ", err: %d", mRelPath.get(), + errno)); + return READ_ERROR; + } + + if (!S_ISDIR(dirInfo.st_mode)) { + LOG(("path present, but not a directory: " LOG_S, mRelPath.get())); + return DELETE_ERROR_EXPECTED_DIR; + } + + rv = NS_taccess(mDir.get(), W_OK); + if (rv) { + LOG(("access failed: %d, %d", rv, errno)); + return WRITE_ERROR_DIR_ACCESS_DENIED; + } + + return OK; +} + +int +RemoveDir::Execute() +{ + if (mSkip) + return OK; + + LOG(("EXECUTE REMOVEDIR " LOG_S "/", mRelPath.get())); + + // The directory is checked for existence at every step since it might have + // been removed by a separate instruction: bug 311099. + int rv = NS_taccess(mDir.get(), F_OK); + if (rv) { + LOG(("directory no longer exists; skipping")); + mSkip = 1; + } + + return OK; +} + +void +RemoveDir::Finish(int status) +{ + if (mSkip || status != OK) + return; + + LOG(("FINISH REMOVEDIR " LOG_S "/", mRelPath.get())); + + // The directory is checked for existence at every step since it might have + // been removed by a separate instruction: bug 311099. + int rv = NS_taccess(mDir.get(), F_OK); + if (rv) { + LOG(("directory no longer exists; skipping")); + return; + } + + + if (status == OK) { + if (NS_trmdir(mDir.get())) { + LOG(("non-fatal error removing directory: " LOG_S "/, rv: %d, err: %d", + mRelPath.get(), rv, errno)); + } + } +} + +class AddFile : public Action +{ +public: + AddFile() : mAdded(false) { } + + virtual int Parse(NS_tchar *line); + virtual int Prepare(); + virtual int Execute(); + virtual void Finish(int status); + +private: + mozilla::UniquePtr<NS_tchar[]> mFile; + mozilla::UniquePtr<NS_tchar[]> mRelPath; + bool mAdded; +}; + +int +AddFile::Parse(NS_tchar *line) +{ + // format "<newfile>" + + NS_tchar * validPath = get_valid_path(&line); + if (!validPath) { + return PARSE_ERROR; + } + + mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mRelPath.get(), validPath); + + mFile.reset(get_full_path(validPath)); + if (!mFile) { + return PARSE_ERROR; + } + + return OK; +} + +int +AddFile::Prepare() +{ + LOG(("PREPARE ADD " LOG_S, mRelPath.get())); + + return OK; +} + +int +AddFile::Execute() +{ + LOG(("EXECUTE ADD " LOG_S, mRelPath.get())); + + int rv; + + // First make sure that we can actually get rid of any existing file. + rv = NS_taccess(mFile.get(), F_OK); + if (rv == 0) { + if (sStagedUpdate) { + // Staged updates don't need backup files so just remove it. + rv = ensure_remove(mFile.get()); + } else { + rv = backup_create(mFile.get()); + } + if (rv) { + return rv; + } + } else { + rv = ensure_parent_dir(mFile.get()); + if (rv) + return rv; + } + +#ifdef XP_WIN + char sourcefile[MAXPATHLEN]; + if (!WideCharToMultiByte(CP_UTF8, 0, mRelPath.get(), -1, sourcefile, + MAXPATHLEN, nullptr, nullptr)) { + LOG(("error converting wchar to utf8: %d", GetLastError())); + return STRING_CONVERSION_ERROR; + } + + rv = gArchiveReader.ExtractFile(sourcefile, mFile.get()); +#else + rv = gArchiveReader.ExtractFile(mRelPath.get(), mFile.get()); +#endif + if (!rv) { + mAdded = true; + } + return rv; +} + +void +AddFile::Finish(int status) +{ + LOG(("FINISH ADD " LOG_S, mRelPath.get())); + // Staged updates don't create backup files. + if (!sStagedUpdate) { + // When there is an update failure and a file has been added it is removed + // here since there might not be a backup to replace it. + if (status && mAdded) { + NS_tremove(mFile.get()); + } + backup_finish(mFile.get(), mRelPath.get(), status); + } +} + +class PatchFile : public Action +{ +public: + PatchFile() : mPatchFile(nullptr), mPatchIndex(-1), buf(nullptr) { } + + virtual ~PatchFile(); + + virtual int Parse(NS_tchar *line); + virtual int Prepare(); // should check for patch file and for checksum here + virtual int Execute(); + virtual void Finish(int status); + +private: + int LoadSourceFile(FILE* ofile); + + static int sPatchIndex; + + const NS_tchar *mPatchFile; + mozilla::UniquePtr<NS_tchar[]> mFile; + mozilla::UniquePtr<NS_tchar[]> mFileRelPath; + int mPatchIndex; + MBSPatchHeader header; + unsigned char *buf; + NS_tchar spath[MAXPATHLEN]; + AutoFile mPatchStream; +}; + +int PatchFile::sPatchIndex = 0; + +PatchFile::~PatchFile() +{ + // Make sure mPatchStream gets unlocked on Windows; the system will do that, + // but not until some indeterminate future time, and we want determinism. + // Normally this happens at the end of Execute, when we close the stream; + // this call is here in case Execute errors out. +#ifdef XP_WIN + if (mPatchStream) { + UnlockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1); + } +#endif + + // delete the temporary patch file + if (spath[0]) { + NS_tremove(spath); + } + + if (buf) { + free(buf); + } +} + +int +PatchFile::LoadSourceFile(FILE* ofile) +{ + struct stat os; + int rv = fstat(fileno((FILE *)ofile), &os); + if (rv) { + LOG(("LoadSourceFile: unable to stat destination file: " LOG_S ", " \ + "err: %d", mFileRelPath.get(), errno)); + return READ_ERROR; + } + + if (uint32_t(os.st_size) != header.slen) { + LOG(("LoadSourceFile: destination file size %d does not match expected size %d", + uint32_t(os.st_size), header.slen)); + return LOADSOURCE_ERROR_WRONG_SIZE; + } + + buf = (unsigned char *) malloc(header.slen); + if (!buf) { + return UPDATER_MEM_ERROR; + } + + size_t r = header.slen; + unsigned char *rb = buf; + while (r) { + const size_t count = mmin(SSIZE_MAX, r); + size_t c = fread(rb, 1, count, ofile); + if (c != count) { + LOG(("LoadSourceFile: error reading destination file: " LOG_S, + mFileRelPath.get())); + return READ_ERROR; + } + + r -= c; + rb += c; + } + + // Verify that the contents of the source file correspond to what we expect. + + unsigned int crc = crc32(buf, header.slen); + + if (crc != header.scrc32) { + LOG(("LoadSourceFile: destination file crc %d does not match expected " \ + "crc %d", crc, header.scrc32)); + return CRC_ERROR; + } + + return OK; +} + +int +PatchFile::Parse(NS_tchar *line) +{ + // format "<patchfile>" "<filetopatch>" + + // Get the path to the patch file inside of the mar + mPatchFile = mstrtok(kQuote, &line); + if (!mPatchFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar *q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + NS_tchar * validPath = get_valid_path(&line); + if (!validPath) { + return PARSE_ERROR; + } + + mFileRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mFileRelPath.get(), validPath); + + mFile.reset(get_full_path(validPath)); + if (!mFile) { + return PARSE_ERROR; + } + + return OK; +} + +int +PatchFile::Prepare() +{ + LOG(("PREPARE PATCH " LOG_S, mFileRelPath.get())); + + // extract the patch to a temporary file + mPatchIndex = sPatchIndex++; + + NS_tsnprintf(spath, sizeof(spath)/sizeof(spath[0]), + NS_T("%s/updating/%d.patch"), gWorkingDirPath, mPatchIndex); + + NS_tremove(spath); + + mPatchStream = NS_tfopen(spath, NS_T("wb+")); + if (!mPatchStream) { + return WRITE_ERROR; + } + +#ifdef XP_WIN + // Lock the patch file, so it can't be messed with between + // when we're done creating it and when we go to apply it. + if (!LockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1)) { + LOG(("Couldn't lock patch file: %d", GetLastError())); + return LOCK_ERROR_PATCH_FILE; + } + + char sourcefile[MAXPATHLEN]; + if (!WideCharToMultiByte(CP_UTF8, 0, mPatchFile, -1, sourcefile, MAXPATHLEN, + nullptr, nullptr)) { + LOG(("error converting wchar to utf8: %d", GetLastError())); + return STRING_CONVERSION_ERROR; + } + + int rv = gArchiveReader.ExtractFileToStream(sourcefile, mPatchStream); +#else + int rv = gArchiveReader.ExtractFileToStream(mPatchFile, mPatchStream); +#endif + + return rv; +} + +int +PatchFile::Execute() +{ + LOG(("EXECUTE PATCH " LOG_S, mFileRelPath.get())); + + fseek(mPatchStream, 0, SEEK_SET); + + int rv = MBS_ReadHeader(mPatchStream, &header); + if (rv) { + return rv; + } + + FILE *origfile = nullptr; +#ifdef XP_WIN + if (NS_tstrcmp(mFileRelPath.get(), gCallbackRelPath) == 0) { + // Read from the copy of the callback when patching since the callback can't + // be opened for reading to prevent the application from being launched. + origfile = NS_tfopen(gCallbackBackupPath, NS_T("rb")); + } else { + origfile = NS_tfopen(mFile.get(), NS_T("rb")); + } +#else + origfile = NS_tfopen(mFile.get(), NS_T("rb")); +#endif + + if (!origfile) { + LOG(("unable to open destination file: " LOG_S ", err: %d", + mFileRelPath.get(), errno)); + return READ_ERROR; + } + + rv = LoadSourceFile(origfile); + fclose(origfile); + if (rv) { + LOG(("LoadSourceFile failed")); + return rv; + } + + // Rename the destination file if it exists before proceeding so it can be + // used to restore the file to its original state if there is an error. + struct NS_tstat_t ss; + rv = NS_tstat(mFile.get(), &ss); + if (rv) { + LOG(("failed to read file status info: " LOG_S ", err: %d", + mFileRelPath.get(), errno)); + return READ_ERROR; + } + + // Staged updates don't need backup files. + if (!sStagedUpdate) { + rv = backup_create(mFile.get()); + if (rv) { + return rv; + } + } + +#if defined(HAVE_POSIX_FALLOCATE) + AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode)); + posix_fallocate(fileno((FILE *)ofile), 0, header.dlen); +#elif defined(XP_WIN) + bool shouldTruncate = true; + // Creating the file, setting the size, and then closing the file handle + // lessens fragmentation more than any other method tested. Other methods that + // have been tested are: + // 1. _chsize / _chsize_s reduced fragmentation though not completely. + // 2. _get_osfhandle and then setting the size reduced fragmentation though + // not completely. There are also reports of _get_osfhandle failing on + // mingw. + HANDLE hfile = CreateFileW(mFile.get(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + if (hfile != INVALID_HANDLE_VALUE) { + if (SetFilePointer(hfile, header.dlen, + nullptr, FILE_BEGIN) != INVALID_SET_FILE_POINTER && + SetEndOfFile(hfile) != 0) { + shouldTruncate = false; + } + CloseHandle(hfile); + } + + AutoFile ofile(ensure_open(mFile.get(), shouldTruncate ? NS_T("wb+") : NS_T("rb+"), + ss.st_mode)); +#elif defined(XP_MACOSX) + AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode)); + // Modified code from FileUtils.cpp + fstore_t store = {F_ALLOCATECONTIG, F_PEOFPOSMODE, 0, header.dlen}; + // Try to get a continous chunk of disk space + rv = fcntl(fileno((FILE *)ofile), F_PREALLOCATE, &store); + if (rv == -1) { + // OK, perhaps we are too fragmented, allocate non-continuous + store.fst_flags = F_ALLOCATEALL; + rv = fcntl(fileno((FILE *)ofile), F_PREALLOCATE, &store); + } + + if (rv != -1) { + ftruncate(fileno((FILE *)ofile), header.dlen); + } +#else + AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode)); +#endif + + if (ofile == nullptr) { + LOG(("unable to create new file: " LOG_S ", err: %d", mFileRelPath.get(), + errno)); + return WRITE_ERROR_OPEN_PATCH_FILE; + } + +#ifdef XP_WIN + if (!shouldTruncate) { + fseek(ofile, 0, SEEK_SET); + } +#endif + + rv = MBS_ApplyPatch(&header, mPatchStream, buf, ofile); + + // Go ahead and do a bit of cleanup now to minimize runtime overhead. + // Make sure mPatchStream gets unlocked on Windows; the system will do that, + // but not until some indeterminate future time, and we want determinism. +#ifdef XP_WIN + UnlockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1); +#endif + // Set mPatchStream to nullptr to make AutoFile close the file, + // so it can be deleted on Windows. + mPatchStream = nullptr; + NS_tremove(spath); + spath[0] = NS_T('\0'); + free(buf); + buf = nullptr; + + return rv; +} + +void +PatchFile::Finish(int status) +{ + LOG(("FINISH PATCH " LOG_S, mFileRelPath.get())); + + // Staged updates don't create backup files. + if (!sStagedUpdate) { + backup_finish(mFile.get(), mFileRelPath.get(), status); + } +} + +class AddIfFile : public AddFile +{ +public: + virtual int Parse(NS_tchar *line); + virtual int Prepare(); + virtual int Execute(); + virtual void Finish(int status); + +protected: + mozilla::UniquePtr<NS_tchar[]> mTestFile; +}; + +int +AddIfFile::Parse(NS_tchar *line) +{ + // format "<testfile>" "<newfile>" + + mTestFile.reset(get_full_path(get_valid_path(&line))); + if (!mTestFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar *q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + return AddFile::Parse(line); +} + +int +AddIfFile::Prepare() +{ + // If the test file does not exist, then skip this action. + if (NS_taccess(mTestFile.get(), F_OK)) { + mTestFile = nullptr; + return OK; + } + + return AddFile::Prepare(); +} + +int +AddIfFile::Execute() +{ + if (!mTestFile) + return OK; + + return AddFile::Execute(); +} + +void +AddIfFile::Finish(int status) +{ + if (!mTestFile) + return; + + AddFile::Finish(status); +} + +class AddIfNotFile : public AddFile +{ +public: + virtual int Parse(NS_tchar *line); + virtual int Prepare(); + virtual int Execute(); + virtual void Finish(int status); + +protected: + mozilla::UniquePtr<NS_tchar[]> mTestFile; +}; + +int +AddIfNotFile::Parse(NS_tchar *line) +{ + // format "<testfile>" "<newfile>" + + mTestFile.reset(get_full_path(get_valid_path(&line))); + if (!mTestFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar *q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + return AddFile::Parse(line); +} + +int +AddIfNotFile::Prepare() +{ + // If the test file exists, then skip this action. + if (!NS_taccess(mTestFile.get(), F_OK)) { + mTestFile = NULL; + return OK; + } + + return AddFile::Prepare(); +} + +int +AddIfNotFile::Execute() +{ + if (!mTestFile) + return OK; + + return AddFile::Execute(); +} + +void +AddIfNotFile::Finish(int status) +{ + if (!mTestFile) + return; + + AddFile::Finish(status); +} + +class PatchIfFile : public PatchFile +{ +public: + virtual int Parse(NS_tchar *line); + virtual int Prepare(); // should check for patch file and for checksum here + virtual int Execute(); + virtual void Finish(int status); + +private: + mozilla::UniquePtr<NS_tchar[]> mTestFile; +}; + +int +PatchIfFile::Parse(NS_tchar *line) +{ + // format "<testfile>" "<patchfile>" "<filetopatch>" + + mTestFile.reset(get_full_path(get_valid_path(&line))); + if (!mTestFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar *q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + return PatchFile::Parse(line); +} + +int +PatchIfFile::Prepare() +{ + // If the test file does not exist, then skip this action. + if (NS_taccess(mTestFile.get(), F_OK)) { + mTestFile = nullptr; + return OK; + } + + return PatchFile::Prepare(); +} + +int +PatchIfFile::Execute() +{ + if (!mTestFile) + return OK; + + return PatchFile::Execute(); +} + +void +PatchIfFile::Finish(int status) +{ + if (!mTestFile) + return; + + PatchFile::Finish(status); +} + +//----------------------------------------------------------------------------- + +#ifdef XP_WIN +#include "nsWindowsRestart.cpp" +#include "nsWindowsHelpers.h" +#include "uachelper.h" +#include "pathhash.h" + +/** + * Launch the post update application (helper.exe). It takes in the path of the + * callback application to calculate the path of helper.exe. For service updates + * this is called from both the system account and the current user account. + * + * @param installationDir The path to the callback application binary. + * @param updateInfoDir The directory where update info is stored. + * @return true if there was no error starting the process. + */ +bool +LaunchWinPostProcess(const WCHAR *installationDir, + const WCHAR *updateInfoDir) +{ + WCHAR workingDirectory[MAX_PATH + 1] = { L'\0' }; + wcsncpy(workingDirectory, installationDir, MAX_PATH); + + // Launch helper.exe to perform post processing (e.g. registry and log file + // modifications) for the update. + WCHAR inifile[MAX_PATH + 1] = { L'\0' }; + wcsncpy(inifile, installationDir, MAX_PATH); + if (!PathAppendSafe(inifile, L"updater.ini")) { + return false; + } + + WCHAR exefile[MAX_PATH + 1]; + WCHAR exearg[MAX_PATH + 1]; + WCHAR exeasync[10]; + bool async = true; + if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeRelPath", nullptr, + exefile, MAX_PATH + 1, inifile)) { + return false; + } + + if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeArg", nullptr, exearg, + MAX_PATH + 1, inifile)) { + return false; + } + + if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeAsync", L"TRUE", + exeasync, + sizeof(exeasync)/sizeof(exeasync[0]), + inifile)) { + return false; + } + + // The relative path must not contain directory traversals, current directory, + // or colons. + if (wcsstr(exefile, L"..") != nullptr || + wcsstr(exefile, L"./") != nullptr || + wcsstr(exefile, L".\\") != nullptr || + wcsstr(exefile, L":") != nullptr) { + return false; + } + + // The relative path must not start with a decimal point, backslash, or + // forward slash. + if (exefile[0] == L'.' || + exefile[0] == L'\\' || + exefile[0] == L'/') { + return false; + } + + WCHAR exefullpath[MAX_PATH + 1] = { L'\0' }; + wcsncpy(exefullpath, installationDir, MAX_PATH); + if (!PathAppendSafe(exefullpath, exefile)) { + return false; + } + + if (!IsValidFullPath(exefullpath)) { + return false; + } + +#if !defined(TEST_UPDATER) && defined(MOZ_MAINTENANCE_SERVICE) + if (sUsingService && + !DoesBinaryMatchAllowedCertificates(installationDir, exefullpath)) { + return false; + } +#endif + + WCHAR dlogFile[MAX_PATH + 1]; + if (!PathGetSiblingFilePath(dlogFile, exefullpath, L"uninstall.update")) { + return false; + } + + WCHAR slogFile[MAX_PATH + 1] = { L'\0' }; + wcsncpy(slogFile, updateInfoDir, MAX_PATH); + if (!PathAppendSafe(slogFile, L"update.log")) { + return false; + } + + WCHAR dummyArg[14] = { L'\0' }; + wcsncpy(dummyArg, L"argv0ignored ", sizeof(dummyArg) / sizeof(dummyArg[0]) - 1); + + size_t len = wcslen(exearg) + wcslen(dummyArg); + WCHAR *cmdline = (WCHAR *) malloc((len + 1) * sizeof(WCHAR)); + if (!cmdline) { + return false; + } + + wcsncpy(cmdline, dummyArg, len); + wcscat(cmdline, exearg); + + if (sUsingService || + !_wcsnicmp(exeasync, L"false", 6) || + !_wcsnicmp(exeasync, L"0", 2)) { + async = false; + } + + // We want to launch the post update helper app to update the Windows + // registry even if there is a failure with removing the uninstall.update + // file or copying the update.log file. + CopyFileW(slogFile, dlogFile, false); + + STARTUPINFOW si = {sizeof(si), 0}; + si.lpDesktop = L""; + PROCESS_INFORMATION pi = {0}; + + bool ok = CreateProcessW(exefullpath, + cmdline, + nullptr, // no special security attributes + nullptr, // no special thread attributes + false, // don't inherit filehandles + 0, // No special process creation flags + nullptr, // inherit my environment + workingDirectory, + &si, + &pi); + free(cmdline); + if (ok) { + if (!async) { + WaitForSingleObject(pi.hProcess, INFINITE); + } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + return ok; +} + +#endif + +static void +LaunchCallbackApp(const NS_tchar *workingDir, + int argc, + NS_tchar **argv, + bool usingService) +{ + putenv(const_cast<char*>("NO_EM_RESTART=")); + putenv(const_cast<char*>("MOZ_LAUNCHED_CHILD=1")); + + // Run from the specified working directory (see bug 312360). This is not + // necessary on Windows CE since the application that launches the updater + // passes the working directory as an --environ: command line argument. + if (NS_tchdir(workingDir) != 0) { + LOG(("Warning: chdir failed")); + } + +#if defined(USE_EXECV) + execv(argv[0], argv); +#elif defined(XP_MACOSX) + LaunchChild(argc, (const char**)argv); +#elif defined(XP_WIN) + // Do not allow the callback to run when running an update through the + // service as session 0. The unelevated updater.exe will do the launching. + if (!usingService) { + WinLaunchChild(argv[0], argc, argv, nullptr); + } +#else +# warning "Need implementaton of LaunchCallbackApp" +#endif +} + +static bool +WriteStatusFile(const char* aStatus) +{ + NS_tchar filename[MAXPATHLEN] = {NS_T('\0')}; +#if defined(XP_WIN) + // The temp file is not removed on failure since there is client code that + // will remove it. + if (GetTempFileNameW(gPatchDirPath, L"sta", 0, filename) == 0) { + return false; + } +#else + NS_tsnprintf(filename, sizeof(filename)/sizeof(filename[0]), + NS_T("%s/update.status"), gPatchDirPath); +#endif + + // Make sure that the directory for the update status file exists + if (ensure_parent_dir(filename)) { + return false; + } + + // This is scoped to make the AutoFile close the file so it is possible to + // move the temp file to the update.status file on Windows. + { + AutoFile file(NS_tfopen(filename, NS_T("wb+"))); + if (file == nullptr) { + return false; + } + + if (fwrite(aStatus, strlen(aStatus), 1, file) != 1) { + return false; + } + } + +#if defined(XP_WIN) + NS_tchar dstfilename[MAXPATHLEN] = {NS_T('\0')}; + NS_tsnprintf(dstfilename, sizeof(dstfilename)/sizeof(dstfilename[0]), + NS_T("%s\\update.status"), gPatchDirPath); + if (MoveFileExW(filename, dstfilename, MOVEFILE_REPLACE_EXISTING) == 0) { + return false; + } +#endif + + return true; +} + +static void +WriteStatusFile(int status) +{ + const char *text; + + char buf[32]; + if (status == OK) { + if (sStagedUpdate) { + text = "applied\n"; + } else { + text = "succeeded\n"; + } + } else { + snprintf(buf, sizeof(buf)/sizeof(buf[0]), "failed: %d\n", status); + text = buf; + } + + WriteStatusFile(text); +} + +#ifdef MOZ_MAINTENANCE_SERVICE +/* + * Read the update.status file and sets isPendingService to true if + * the status is set to pending-service. + * + * @param isPendingService Out parameter for specifying if the status + * is set to pending-service or not. + * @return true if the information was retrieved and it is pending + * or pending-service. +*/ +static bool +IsUpdateStatusPendingService() +{ + NS_tchar filename[MAXPATHLEN]; + NS_tsnprintf(filename, sizeof(filename)/sizeof(filename[0]), + NS_T("%s/update.status"), gPatchDirPath); + + AutoFile file(NS_tfopen(filename, NS_T("rb"))); + if (file == nullptr) + return false; + + char buf[32] = { 0 }; + fread(buf, sizeof(buf), 1, file); + + const char kPendingService[] = "pending-service"; + const char kAppliedService[] = "applied-service"; + + return (strncmp(buf, kPendingService, + sizeof(kPendingService) - 1) == 0) || + (strncmp(buf, kAppliedService, + sizeof(kAppliedService) - 1) == 0); +} +#endif + +#ifdef XP_WIN +/* + * Read the update.status file and sets isSuccess to true if + * the status is set to succeeded. + * + * @param isSucceeded Out parameter for specifying if the status + * is set to succeeded or not. + * @return true if the information was retrieved and it is succeeded. +*/ +static bool +IsUpdateStatusSucceeded(bool &isSucceeded) +{ + isSucceeded = false; + NS_tchar filename[MAXPATHLEN]; + NS_tsnprintf(filename, sizeof(filename)/sizeof(filename[0]), + NS_T("%s/update.status"), gPatchDirPath); + + AutoFile file(NS_tfopen(filename, NS_T("rb"))); + if (file == nullptr) + return false; + + char buf[32] = { 0 }; + fread(buf, sizeof(buf), 1, file); + + const char kSucceeded[] = "succeeded"; + isSucceeded = strncmp(buf, kSucceeded, + sizeof(kSucceeded) - 1) == 0; + return true; +} +#endif + +/* + * Copy the entire contents of the application installation directory to the + * destination directory for the update process. + * + * @return 0 if successful, an error code otherwise. + */ +static int +CopyInstallDirToDestDir() +{ + // These files should not be copied over to the updated app +#ifdef XP_WIN +#define SKIPLIST_COUNT 3 +#elif XP_MACOSX +#define SKIPLIST_COUNT 0 +#else +#define SKIPLIST_COUNT 2 +#endif + copy_recursive_skiplist<SKIPLIST_COUNT> skiplist; +#ifndef XP_MACOSX + skiplist.append(0, gInstallDirPath, NS_T("updated")); + skiplist.append(1, gInstallDirPath, NS_T("updates/0")); +#ifdef XP_WIN + skiplist.append(2, gInstallDirPath, NS_T("updated.update_in_progress.lock")); +#endif +#endif + + return ensure_copy_recursive(gInstallDirPath, gWorkingDirPath, skiplist); +} + +/* + * Replace the application installation directory with the destination + * directory in order to finish a staged update task + * + * @return 0 if successful, an error code otherwise. + */ +static int +ProcessReplaceRequest() +{ + // The replacement algorithm is like this: + // 1. Move destDir to tmpDir. In case of failure, abort. + // 2. Move newDir to destDir. In case of failure, revert step 1 and abort. + // 3. Delete tmpDir (or defer it to the next reboot). + +#ifdef XP_MACOSX + NS_tchar destDir[MAXPATHLEN]; + NS_tsnprintf(destDir, sizeof(destDir)/sizeof(destDir[0]), + NS_T("%s/Contents"), gInstallDirPath); +#elif XP_WIN + // Windows preserves the case of the file/directory names. We use the + // GetLongPathName API in order to get the correct case for the directory + // name, so that if the user has used a different case when launching the + // application, the installation directory's name does not change. + NS_tchar destDir[MAXPATHLEN]; + if (!GetLongPathNameW(gInstallDirPath, destDir, + sizeof(destDir)/sizeof(destDir[0]))) { + return NO_INSTALLDIR_ERROR; + } +#else + NS_tchar* destDir = gInstallDirPath; +#endif + + NS_tchar tmpDir[MAXPATHLEN]; + NS_tsnprintf(tmpDir, sizeof(tmpDir)/sizeof(tmpDir[0]), + NS_T("%s.bak"), destDir); + + NS_tchar newDir[MAXPATHLEN]; + NS_tsnprintf(newDir, sizeof(newDir)/sizeof(newDir[0]), +#ifdef XP_MACOSX + NS_T("%s/Contents"), + gWorkingDirPath); +#else + NS_T("%s.bak/updated"), + gInstallDirPath); +#endif + + // First try to remove the possibly existing temp directory, because if this + // directory exists, we will fail to rename destDir. + // No need to error check here because if this fails, we will fail in the + // next step anyways. + ensure_remove_recursive(tmpDir); + + LOG(("Begin moving destDir (" LOG_S ") to tmpDir (" LOG_S ")", + destDir, tmpDir)); + int rv = rename_file(destDir, tmpDir, true); +#ifdef XP_WIN + // On Windows, if Firefox is launched using the shortcut, it will hold a handle + // to its installation directory open, which might not get released in time. + // Therefore we wait a little bit here to see if the handle is released. + // If it's not released, we just fail to perform the replace request. + const int max_retries = 10; + int retries = 0; + while (rv == WRITE_ERROR && (retries++ < max_retries)) { + LOG(("PerformReplaceRequest: destDir rename attempt %d failed. " \ + "File: " LOG_S ". Last error: %d, err: %d", retries, + destDir, GetLastError(), rv)); + + Sleep(100); + + rv = rename_file(destDir, tmpDir, true); + } +#endif + if (rv) { + // The status file will have 'pending' written to it so there is no value in + // returning an error specific for this failure. + LOG(("Moving destDir to tmpDir failed, err: %d", rv)); + return rv; + } + + LOG(("Begin moving newDir (" LOG_S ") to destDir (" LOG_S ")", + newDir, destDir)); + rv = rename_file(newDir, destDir, true); +#ifdef XP_MACOSX + if (rv) { + LOG(("Moving failed. Begin copying newDir (" LOG_S ") to destDir (" LOG_S ")", + newDir, destDir)); + copy_recursive_skiplist<0> skiplist; + rv = ensure_copy_recursive(newDir, destDir, skiplist); + } +#endif + if (rv) { + LOG(("Moving newDir to destDir failed, err: %d", rv)); + LOG(("Now, try to move tmpDir back to destDir")); + ensure_remove_recursive(destDir); + int rv2 = rename_file(tmpDir, destDir, true); + if (rv2) { + LOG(("Moving tmpDir back to destDir failed, err: %d", rv2)); + } + // The status file will be have 'pending' written to it so there is no value + // in returning an error specific for this failure. + return rv; + } + +#if !defined(XP_WIN) && !defined(XP_MACOSX) + // Platforms that have their updates directory in the installation directory + // need to have the last-update.log and backup-update.log files moved from the + // old installation directory to the new installation directory. + NS_tchar tmpLog[MAXPATHLEN]; + NS_tsnprintf(tmpLog, sizeof(tmpLog)/sizeof(tmpLog[0]), + NS_T("%s/updates/last-update.log"), tmpDir); + if (!NS_taccess(tmpLog, F_OK)) { + NS_tchar destLog[MAXPATHLEN]; + NS_tsnprintf(destLog, sizeof(destLog)/sizeof(destLog[0]), + NS_T("%s/updates/last-update.log"), destDir); + NS_tremove(destLog); + NS_trename(tmpLog, destLog); + } +#endif + + LOG(("Now, remove the tmpDir")); + rv = ensure_remove_recursive(tmpDir, true); + if (rv) { + LOG(("Removing tmpDir failed, err: %d", rv)); +#ifdef XP_WIN + NS_tchar deleteDir[MAXPATHLEN]; + NS_tsnprintf(deleteDir, sizeof(deleteDir)/sizeof(deleteDir[0]), + NS_T("%s\\%s"), destDir, DELETE_DIR); + // Attempt to remove the tobedeleted directory and then recreate it if it + // was successfully removed. + _wrmdir(deleteDir); + if (NS_taccess(deleteDir, F_OK)) { + NS_tmkdir(deleteDir, 0755); + } + remove_recursive_on_reboot(tmpDir, deleteDir); +#endif + } + +#ifdef XP_MACOSX + // On OS X, we we need to remove the staging directory after its Contents + // directory has been moved. + NS_tchar updatedAppDir[MAXPATHLEN]; + NS_tsnprintf(updatedAppDir, sizeof(updatedAppDir)/sizeof(updatedAppDir[0]), + NS_T("%s/Updated.app"), gPatchDirPath); + ensure_remove_recursive(updatedAppDir); +#endif + + gSucceeded = true; + + return 0; +} + +#ifdef XP_WIN +static void +WaitForServiceFinishThread(void *param) +{ + // We wait at most 10 minutes, we already waited 5 seconds previously + // before deciding to show this UI. + WaitForServiceStop(SVC_NAME, 595); + QuitProgressUI(); +} +#endif + +#ifdef MOZ_VERIFY_MAR_SIGNATURE +/** + * This function reads in the ACCEPTED_MAR_CHANNEL_IDS from update-settings.ini + * + * @param path The path to the ini file that is to be read + * @param results A pointer to the location to store the read strings + * @return OK on success + */ +static int +ReadMARChannelIDs(const NS_tchar *path, MARChannelStringTable *results) +{ + const unsigned int kNumStrings = 1; + const char *kUpdaterKeys = "ACCEPTED_MAR_CHANNEL_IDS\0"; + char updater_strings[kNumStrings][MAX_TEXT_LEN]; + + int result = ReadStrings(path, kUpdaterKeys, kNumStrings, + updater_strings, "Settings"); + + strncpy(results->MARChannelID, updater_strings[0], MAX_TEXT_LEN - 1); + results->MARChannelID[MAX_TEXT_LEN - 1] = 0; + + return result; +} +#endif + +static int +GetUpdateFileName(NS_tchar *fileName, int maxChars) +{ +#if defined(MOZ_WIDGET_GONK) + // If an update.link file exists, then it will contain the name + // of the update file (terminated by a newline). + + NS_tchar linkFileName[MAXPATHLEN]; + NS_tsnprintf(linkFileName, sizeof(linkFileName)/sizeof(linkFileName[0]), + NS_T("%s/update.link"), gPatchDirPath); + AutoFile linkFile(NS_tfopen(linkFileName, NS_T("rb"))); + if (linkFile == nullptr) { + NS_tsnprintf(fileName, maxChars, + NS_T("%s/update.mar"), gPatchDirPath); + return OK; + } + + char dataFileName[MAXPATHLEN]; + size_t bytesRead; + + if ((bytesRead = fread(dataFileName, 1, sizeof(dataFileName)-1, linkFile)) <= 0) { + *fileName = NS_T('\0'); + return READ_ERROR; + } + if (dataFileName[bytesRead-1] == '\n') { + // Strip trailing newline (for \n and \r\n) + bytesRead--; + } + if (dataFileName[bytesRead-1] == '\r') { + // Strip trailing CR (for \r, \r\n) + bytesRead--; + } + dataFileName[bytesRead] = '\0'; + + strncpy(fileName, dataFileName, maxChars-1); + fileName[maxChars-1] = '\0'; +#else + // We currently only support update.link files under GONK + NS_tsnprintf(fileName, maxChars, + NS_T("%s/update.mar"), gPatchDirPath); +#endif + return OK; +} + +static void +UpdateThreadFunc(void *param) +{ + // open ZIP archive and process... + int rv; + if (sReplaceRequest) { + rv = ProcessReplaceRequest(); + } else { + NS_tchar dataFile[MAXPATHLEN]; + rv = GetUpdateFileName(dataFile, sizeof(dataFile)/sizeof(dataFile[0])); + if (rv == OK) { + rv = gArchiveReader.Open(dataFile); + } + +#ifdef MOZ_VERIFY_MAR_SIGNATURE + if (rv == OK) { +#ifdef XP_WIN + HKEY baseKey = nullptr; + wchar_t valueName[] = L"Image Path"; + wchar_t rasenh[] = L"rsaenh.dll"; + bool reset = false; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Cryptography\\Defaults\\Provider\\Microsoft Enhanced Cryptographic Provider v1.0", + 0, KEY_READ | KEY_WRITE, + &baseKey) == ERROR_SUCCESS) { + wchar_t path[MAX_PATH + 1]; + DWORD size = sizeof(path); + DWORD type; + if (RegQueryValueExW(baseKey, valueName, 0, &type, + (LPBYTE)path, &size) == ERROR_SUCCESS) { + if (type == REG_SZ && wcscmp(path, rasenh) == 0) { + wchar_t rasenhFullPath[] = L"%SystemRoot%\\System32\\rsaenh.dll"; + if (RegSetValueExW(baseKey, valueName, 0, REG_SZ, + (const BYTE*)rasenhFullPath, + sizeof(rasenhFullPath)) == ERROR_SUCCESS) { + reset = true; + } + } + } + } +#endif + rv = gArchiveReader.VerifySignature(); +#ifdef XP_WIN + if (baseKey) { + if (reset) { + RegSetValueExW(baseKey, valueName, 0, REG_SZ, + (const BYTE*)rasenh, + sizeof(rasenh)); + } + RegCloseKey(baseKey); + } +#endif + } + + if (rv == OK) { + if (rv == OK) { + NS_tchar updateSettingsPath[MAX_TEXT_LEN]; + NS_tsnprintf(updateSettingsPath, + sizeof(updateSettingsPath) / sizeof(updateSettingsPath[0]), +#ifdef XP_MACOSX + NS_T("%s/Contents/Resources/update-settings.ini"), +#else + NS_T("%s/update-settings.ini"), +#endif + gWorkingDirPath); + MARChannelStringTable MARStrings; + if (ReadMARChannelIDs(updateSettingsPath, &MARStrings) != OK) { + // If we can't read from update-settings.ini then we shouldn't impose + // a MAR restriction. Some installations won't even include this file. + MARStrings.MARChannelID[0] = '\0'; + } + + rv = gArchiveReader.VerifyProductInformation(MARStrings.MARChannelID, + MOZ_APP_VERSION); + } + } +#endif + + if (rv == OK && sStagedUpdate && !sIsOSUpdate) { +#ifdef TEST_UPDATER + // The MOZ_TEST_SKIP_UPDATE_STAGE environment variable prevents copying + // the files in dist/bin in the test updater when staging an update since + // this can cause tests to timeout. + if (EnvHasValue("MOZ_TEST_SKIP_UPDATE_STAGE")) { + rv = OK; + } else { + rv = CopyInstallDirToDestDir(); + } +#else + rv = CopyInstallDirToDestDir(); +#endif + } + + if (rv == OK) { + rv = DoUpdate(); + gArchiveReader.Close(); + NS_tchar updatingDir[MAXPATHLEN]; + NS_tsnprintf(updatingDir, sizeof(updatingDir)/sizeof(updatingDir[0]), + NS_T("%s/updating"), gWorkingDirPath); + ensure_remove_recursive(updatingDir); + } + } + + if (rv && (sReplaceRequest || sStagedUpdate)) { +#ifdef XP_WIN + // On Windows, the current working directory of the process should be changed + // so that it's not locked. + if (sStagedUpdate) { + NS_tchar sysDir[MAX_PATH + 1] = { L'\0' }; + if (GetSystemDirectoryW(sysDir, MAX_PATH + 1)) { + NS_tchdir(sysDir); + } + } +#endif + ensure_remove_recursive(gWorkingDirPath); + // When attempting to replace the application, we should fall back + // to non-staged updates in case of a failure. We do this by + // setting the status to pending, exiting the updater, and + // launching the callback application. The callback application's + // startup path will see the pending status, and will start the + // updater application again in order to apply the update without + // staging. + if (sReplaceRequest) { + WriteStatusFile(sUsingService ? "pending-service" : "pending"); + } else { + WriteStatusFile(rv); + } +#ifdef TEST_UPDATER + // Some tests need to use --test-process-updates again. + putenv(const_cast<char*>("MOZ_TEST_PROCESS_UPDATES=")); +#endif + } else { + if (rv) { + LOG(("failed: %d", rv)); + } else { +#ifdef XP_MACOSX + // If the update was successful we need to update the timestamp on the + // top-level Mac OS X bundle directory so that Mac OS X's Launch Services + // picks up any major changes when the bundle is updated. + if (!sStagedUpdate && utimes(gInstallDirPath, nullptr) != 0) { + LOG(("Couldn't set access/modification time on application bundle.")); + } +#endif + + LOG(("succeeded")); + } + WriteStatusFile(rv); + } + + LOG(("calling QuitProgressUI")); + QuitProgressUI(); +} + +#ifdef XP_MACOSX +static void +ServeElevatedUpdateThreadFunc(void* param) +{ + UpdateServerThreadArgs* threadArgs = (UpdateServerThreadArgs*)param; + gSucceeded = ServeElevatedUpdate(threadArgs->argc, threadArgs->argv); + if (!gSucceeded) { + WriteStatusFile(ELEVATION_CANCELED); + } + QuitProgressUI(); +} + +void freeArguments(int argc, char** argv) +{ + for (int i = 0; i < argc; i++) { + free(argv[i]); + } + free(argv); +} +#endif + +int LaunchCallbackAndPostProcessApps(int argc, NS_tchar** argv, + int callbackIndex +#ifdef XP_WIN + , const WCHAR* elevatedLockFilePath + , HANDLE updateLockFileHandle +#elif XP_MACOSX + , bool isElevated +#endif + ) +{ + if (argc > callbackIndex) { +#if defined(XP_WIN) + if (gSucceeded) { + if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) { + fprintf(stderr, "The post update process was not launched"); + } + + // The service update will only be executed if it is already installed. + // For first time installs of the service, the install will happen from + // the PostUpdate process. We do the service update process here + // because it's possible we are updating with updater.exe without the + // service if the service failed to apply the update. We want to update + // the service to a newer version in that case. If we are not running + // through the service, then MOZ_USING_SERVICE will not exist. + if (!sUsingService) { + StartServiceUpdate(gInstallDirPath); + } + } + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 0); +#elif XP_MACOSX + if (!isElevated) { + if (gSucceeded) { + LaunchMacPostProcess(gInstallDirPath); + } +#endif + + LaunchCallbackApp(argv[5], + argc - callbackIndex, + argv + callbackIndex, + sUsingService); +#ifdef XP_MACOSX + } // if (!isElevated) +#endif /* XP_MACOSX */ + } + return 0; +} + +int NS_main(int argc, NS_tchar **argv) +{ + // The callback is the remaining arguments starting at callbackIndex. + // The argument specified by callbackIndex is the callback executable and the + // argument prior to callbackIndex is the working directory. + const int callbackIndex = 6; + +#ifdef XP_MACOSX + bool isElevated = + strstr(argv[0], "/Library/PrivilegedHelperTools/org.mozilla.updater") != 0; + if (isElevated) { + if (!ObtainUpdaterArguments(&argc, &argv)) { + // Won't actually get here because ObtainUpdaterArguments will terminate + // the current process on failure. + return 1; + } + } +#endif + +#if defined(MOZ_WIDGET_GONK) + if (EnvHasValue("LD_PRELOAD")) { + // If the updater is launched with LD_PRELOAD set, then we wind up + // preloading libmozglue.so. Under some circumstances, this can cause + // the remount of /system to fail when going from rw to ro, so if we + // detect LD_PRELOAD we unsetenv it and relaunch ourselves without it. + // This will cause the offending preloaded library to be closed. + // + // For a variety of reasons, this is really hard to do in a safe manner + // in the parent process, so we do it here. + unsetenv("LD_PRELOAD"); + execv(argv[0], argv); + __android_log_print(ANDROID_LOG_INFO, "updater", + "execve failed: errno: %d. Exiting...", errno); + _exit(1); + } +#endif + +#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && \ + !defined(XP_MACOSX) && !defined(MOZ_WIDGET_GONK) + // On Windows and Mac we rely on native APIs to do verifications so we don't + // need to initialize NSS at all there. + // Otherwise, minimize the amount of NSS we depend on by avoiding all the NSS + // databases. + if (NSS_NoDB_Init(NULL) != SECSuccess) { + PRErrorCode error = PR_GetError(); + fprintf(stderr, "Could not initialize NSS: %s (%d)", + PR_ErrorToName(error), (int) error); + _exit(1); + } +#endif + +#ifdef XP_MACOSX + if (!isElevated) { +#endif + InitProgressUI(&argc, &argv); +#ifdef XP_MACOSX + } +#endif + + // To process an update the updater command line must at a minimum have the + // directory path containing the updater.mar file to process as the first + // argument, the install directory as the second argument, and the directory + // to apply the update to as the third argument. When the updater is launched + // by another process the PID of the parent process should be provided in the + // optional fourth argument and the updater will wait on the parent process to + // exit if the value is non-zero and the process is present. This is necessary + // due to not being able to update files that are in use on Windows. The + // optional fifth argument is the callback's working directory and the + // optional sixth argument is the callback path. The callback is the + // application to launch after updating and it will be launched when these + // arguments are provided whether the update was successful or not. All + // remaining arguments are optional and are passed to the callback when it is + // launched. + if (argc < 4) { + fprintf(stderr, "Usage: updater patch-dir install-dir apply-to-dir [wait-pid [callback-working-dir callback-path args...]]\n"); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + // This check is also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (!IsValidFullPath(argv[1])) { + // Since the status file is written to the patch directory and the patch + // directory is invalid don't write the status file. + fprintf(stderr, "The patch directory path is not valid for this " \ + "application (" LOG_S ")\n", argv[1]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + // The directory containing the update information. + NS_tstrncpy(gPatchDirPath, argv[1], MAXPATHLEN); + + // This check is also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (!IsValidFullPath(argv[2])) { + WriteStatusFile(INVALID_INSTALL_DIR_PATH_ERROR); + fprintf(stderr, "The install directory path is not valid for this " \ + "application (" LOG_S ")\n", argv[2]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + // The directory we're going to update to. + // We copy this string because we need to remove trailing slashes. The C++ + // standard says that it's always safe to write to strings pointed to by argv + // elements, but I don't necessarily believe it. + NS_tstrncpy(gInstallDirPath, argv[2], MAXPATHLEN); + gInstallDirPath[MAXPATHLEN - 1] = NS_T('\0'); + NS_tchar *slash = NS_tstrrchr(gInstallDirPath, NS_SLASH); + if (slash && !slash[1]) { + *slash = NS_T('\0'); + } + +#ifdef XP_WIN + bool useService = false; + bool testOnlyFallbackKeyExists = false; + bool noServiceFallback = false; + + // We never want the service to be used unless we build with + // the maintenance service. +#ifdef MOZ_MAINTENANCE_SERVICE + useService = IsUpdateStatusPendingService(); +#ifdef TEST_UPDATER + noServiceFallback = EnvHasValue("MOZ_NO_SERVICE_FALLBACK"); + putenv(const_cast<char*>("MOZ_NO_SERVICE_FALLBACK=")); + // Our tests run with a different apply directory for each test. + // We use this registry key on our test slaves to store the + // allowed name/issuers. + testOnlyFallbackKeyExists = DoesFallbackKeyExist(); +#endif +#endif + + // Remove everything except close window from the context menu + { + HKEY hkApp = nullptr; + RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Classes\\Applications", + 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr, + &hkApp, nullptr); + RegCloseKey(hkApp); + if (RegCreateKeyExW(HKEY_CURRENT_USER, + L"Software\\Classes\\Applications\\updater.exe", + 0, nullptr, REG_OPTION_VOLATILE, KEY_SET_VALUE, nullptr, + &hkApp, nullptr) == ERROR_SUCCESS) { + RegSetValueExW(hkApp, L"IsHostApp", 0, REG_NONE, 0, 0); + RegSetValueExW(hkApp, L"NoOpenWith", 0, REG_NONE, 0, 0); + RegSetValueExW(hkApp, L"NoStartPage", 0, REG_NONE, 0, 0); + RegCloseKey(hkApp); + } + } +#endif + + // If there is a PID specified and it is not '0' then wait for the process to exit. +#ifdef XP_WIN + __int64 pid = 0; +#else + int pid = 0; +#endif + if (argc > 4) { +#ifdef XP_WIN + pid = _wtoi64(argv[4]); +#else + pid = atoi(argv[4]); +#endif + if (pid == -1) { + // This is a signal from the parent process that the updater should stage + // the update. + sStagedUpdate = true; + } else if (NS_tstrstr(argv[4], NS_T("/replace"))) { + // We're processing a request to replace the application with a staged + // update. + sReplaceRequest = true; + } + } + + // This check is also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (!IsValidFullPath(argv[3])) { + WriteStatusFile(INVALID_WORKING_DIR_PATH_ERROR); + fprintf(stderr, "The working directory path is not valid for this " \ + "application (" LOG_S ")\n", argv[3]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + // The directory we're going to update to. + // We copy this string because we need to remove trailing slashes. The C++ + // standard says that it's always safe to write to strings pointed to by argv + // elements, but I don't necessarily believe it. + NS_tstrncpy(gWorkingDirPath, argv[3], MAXPATHLEN); + gWorkingDirPath[MAXPATHLEN - 1] = NS_T('\0'); + slash = NS_tstrrchr(gWorkingDirPath, NS_SLASH); + if (slash && !slash[1]) { + *slash = NS_T('\0'); + } + + // These checks are also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (argc > callbackIndex) { + if (!IsValidFullPath(argv[callbackIndex])) { + WriteStatusFile(INVALID_CALLBACK_PATH_ERROR); + fprintf(stderr, "The callback file path is not valid for this " \ + "application (" LOG_S ")\n", argv[callbackIndex]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + size_t len = NS_tstrlen(gInstallDirPath); + NS_tchar callbackInstallDir[MAXPATHLEN] = { NS_T('\0') }; + NS_tstrncpy(callbackInstallDir, argv[callbackIndex], len); + if (NS_tstrcmp(gInstallDirPath, callbackInstallDir) != 0) { + WriteStatusFile(INVALID_CALLBACK_DIR_ERROR); + fprintf(stderr, "The callback file must be located in the " \ + "installation directory (" LOG_S ")\n", argv[callbackIndex]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + } + +#ifdef XP_MACOSX + if (!isElevated && !IsRecursivelyWritable(argv[2])) { + // If the app directory isn't recursively writeable, an elevated update is + // required. + UpdateServerThreadArgs threadArgs; + threadArgs.argc = argc; + threadArgs.argv = const_cast<const NS_tchar**>(argv); + + Thread t1; + if (t1.Run(ServeElevatedUpdateThreadFunc, &threadArgs) == 0) { + // Show an indeterminate progress bar while an elevated update is in + // progress. + ShowProgressUI(true); + } + t1.Join(); + + LaunchCallbackAndPostProcessApps(argc, argv, callbackIndex, false); + return gSucceeded ? 0 : 1; + } +#endif + + if (EnvHasValue("MOZ_OS_UPDATE")) { + sIsOSUpdate = true; + putenv(const_cast<char*>("MOZ_OS_UPDATE=")); + } + + LogInit(gPatchDirPath, NS_T("update.log")); + + if (!WriteStatusFile("applying")) { + LOG(("failed setting status to 'applying'")); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + if (sStagedUpdate) { + LOG(("Performing a staged update")); + } else if (sReplaceRequest) { + LOG(("Performing a replace request")); + } + + LOG(("PATCH DIRECTORY " LOG_S, gPatchDirPath)); + LOG(("INSTALLATION DIRECTORY " LOG_S, gInstallDirPath)); + LOG(("WORKING DIRECTORY " LOG_S, gWorkingDirPath)); + +#if defined(XP_WIN) + // These checks are also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (_wcsnicmp(gWorkingDirPath, gInstallDirPath, MAX_PATH) != 0) { + if (!sStagedUpdate && !sReplaceRequest) { + WriteStatusFile(INVALID_APPLYTO_DIR_ERROR); + LOG(("Installation directory and working directory must be the same " + "for non-staged updates. Exiting.")); + LogFinish(); + return 1; + } + + NS_tchar workingDirParent[MAX_PATH]; + NS_tsnprintf(workingDirParent, + sizeof(workingDirParent) / sizeof(workingDirParent[0]), + NS_T("%s"), gWorkingDirPath); + if (!PathRemoveFileSpecW(workingDirParent)) { + WriteStatusFile(REMOVE_FILE_SPEC_ERROR); + LOG(("Error calling PathRemoveFileSpecW: %d", GetLastError())); + LogFinish(); + return 1; + } + + if (_wcsnicmp(workingDirParent, gInstallDirPath, MAX_PATH) != 0) { + WriteStatusFile(INVALID_APPLYTO_DIR_STAGED_ERROR); + LOG(("The apply-to directory must be the same as or " + "a child of the installation directory! Exiting.")); + LogFinish(); + return 1; + } + } +#endif + +#ifdef MOZ_WIDGET_GONK + const char *prioEnv = getenv("MOZ_UPDATER_PRIO"); + if (prioEnv) { + int32_t prioVal; + int32_t oomScoreAdj; + int32_t ioprioClass; + int32_t ioprioLevel; + if (sscanf(prioEnv, "%d/%d/%d/%d", + &prioVal, &oomScoreAdj, &ioprioClass, &ioprioLevel) == 4) { + LOG(("MOZ_UPDATER_PRIO=%s", prioEnv)); + if (setpriority(PRIO_PROCESS, 0, prioVal)) { + LOG(("setpriority(%d) failed, errno = %d", prioVal, errno)); + } + if (ioprio_set(IOPRIO_WHO_PROCESS, 0, + IOPRIO_PRIO_VALUE(ioprioClass, ioprioLevel))) { + LOG(("ioprio_set(%d,%d) failed: errno = %d", + ioprioClass, ioprioLevel, errno)); + } + FILE *fs = fopen("/proc/self/oom_score_adj", "w"); + if (fs) { + fprintf(fs, "%d", oomScoreAdj); + fclose(fs); + } else { + LOG(("Unable to open /proc/self/oom_score_adj for writing, errno = %d", + errno)); + } + } + } +#endif + +#ifdef XP_WIN + if (pid > 0) { + HANDLE parent = OpenProcess(SYNCHRONIZE, false, (DWORD) pid); + // May return nullptr if the parent process has already gone away. + // Otherwise, wait for the parent process to exit before starting the + // update. + if (parent) { + DWORD waitTime = PARENT_WAIT; + DWORD result = WaitForSingleObject(parent, waitTime); + CloseHandle(parent); + if (result != WAIT_OBJECT_0) { + return 1; + } + } + } +#else + if (pid > 0) + waitpid(pid, nullptr, 0); +#endif + +#ifdef XP_WIN +#ifdef MOZ_MAINTENANCE_SERVICE + sUsingService = EnvHasValue("MOZ_USING_SERVICE"); + putenv(const_cast<char*>("MOZ_USING_SERVICE=")); +#endif + // lastFallbackError keeps track of the last error for the service not being + // used, in case of an error when fallback is not enabled we write the + // error to the update.status file. + // When fallback is disabled (MOZ_NO_SERVICE_FALLBACK does not exist) then + // we will instead fallback to not using the service and display a UAC prompt. + int lastFallbackError = FALLBACKKEY_UNKNOWN_ERROR; + + // Launch a second instance of the updater with the runas verb on Windows + // when write access is denied to the installation directory. + HANDLE updateLockFileHandle = INVALID_HANDLE_VALUE; + NS_tchar elevatedLockFilePath[MAXPATHLEN] = {NS_T('\0')}; + if (!sUsingService && + (argc > callbackIndex || sStagedUpdate || sReplaceRequest)) { + NS_tchar updateLockFilePath[MAXPATHLEN]; + if (sStagedUpdate) { + // When staging an update, the lock file is: + // <install_dir>\updated.update_in_progress.lock + NS_tsnprintf(updateLockFilePath, + sizeof(updateLockFilePath)/sizeof(updateLockFilePath[0]), + NS_T("%s/updated.update_in_progress.lock"), gInstallDirPath); + } else if (sReplaceRequest) { + // When processing a replace request, the lock file is: + // <install_dir>\..\moz_update_in_progress.lock + NS_tchar installDir[MAXPATHLEN]; + NS_tstrcpy(installDir, gInstallDirPath); + NS_tchar *slash = (NS_tchar *) NS_tstrrchr(installDir, NS_SLASH); + *slash = NS_T('\0'); + NS_tsnprintf(updateLockFilePath, + sizeof(updateLockFilePath)/sizeof(updateLockFilePath[0]), + NS_T("%s\\moz_update_in_progress.lock"), installDir); + } else { + // In the non-staging update case, the lock file is: + // <install_dir>\<app_name>.exe.update_in_progress.lock + NS_tsnprintf(updateLockFilePath, + sizeof(updateLockFilePath)/sizeof(updateLockFilePath[0]), + NS_T("%s.update_in_progress.lock"), argv[callbackIndex]); + } + + // The update_in_progress.lock file should only exist during an update. In + // case it exists attempt to remove it and exit if that fails to prevent + // simultaneous updates occurring. + if (!_waccess(updateLockFilePath, F_OK) && + NS_tremove(updateLockFilePath) != 0) { + // Try to fall back to the old way of doing updates if a staged + // update fails. + if (sStagedUpdate || sReplaceRequest) { + // Note that this could fail, but if it does, there isn't too much we + // can do in order to recover anyways. + WriteStatusFile("pending"); + } + LOG(("Update already in progress! Exiting")); + return 1; + } + + updateLockFileHandle = CreateFileW(updateLockFilePath, + GENERIC_READ | GENERIC_WRITE, + 0, + nullptr, + OPEN_ALWAYS, + FILE_FLAG_DELETE_ON_CLOSE, + nullptr); + + NS_tsnprintf(elevatedLockFilePath, + sizeof(elevatedLockFilePath)/sizeof(elevatedLockFilePath[0]), + NS_T("%s/update_elevated.lock"), gPatchDirPath); + + // Even if a file has no sharing access, you can still get its attributes + bool startedFromUnelevatedUpdater = + GetFileAttributesW(elevatedLockFilePath) != INVALID_FILE_ATTRIBUTES; + + // If we're running from the service, then we were started with the same + // token as the service so the permissions are already dropped. If we're + // running from an elevated updater that was started from an unelevated + // updater, then we drop the permissions here. We do not drop the + // permissions on the originally called updater because we use its token + // to start the callback application. + if (startedFromUnelevatedUpdater) { + // Disable every privilege we don't need. Processes started using + // CreateProcess will use the same token as this process. + UACHelper::DisablePrivileges(nullptr); + } + + if (updateLockFileHandle == INVALID_HANDLE_VALUE || + (useService && testOnlyFallbackKeyExists && noServiceFallback)) { + if (!_waccess(elevatedLockFilePath, F_OK) && + NS_tremove(elevatedLockFilePath) != 0) { + fprintf(stderr, "Unable to create elevated lock file! Exiting\n"); + return 1; + } + + HANDLE elevatedFileHandle; + elevatedFileHandle = CreateFileW(elevatedLockFilePath, + GENERIC_READ | GENERIC_WRITE, + 0, + nullptr, + OPEN_ALWAYS, + FILE_FLAG_DELETE_ON_CLOSE, + nullptr); + + if (elevatedFileHandle == INVALID_HANDLE_VALUE) { + LOG(("Unable to create elevated lock file! Exiting")); + return 1; + } + + wchar_t *cmdLine = MakeCommandLine(argc - 1, argv + 1); + if (!cmdLine) { + CloseHandle(elevatedFileHandle); + return 1; + } + + // Make sure the path to the updater to use for the update is on local. + // We do this check to make sure that file locking is available for + // race condition security checks. + if (useService) { + BOOL isLocal = FALSE; + useService = IsLocalFile(argv[0], isLocal) && isLocal; + } + + // If we have unprompted elevation we should NOT use the service + // for the update. Service updates happen with the SYSTEM account + // which has more privs than we need to update with. + // Windows 8 provides a user interface so users can configure this + // behavior and it can be configured in the registry in all Windows + // versions that support UAC. + if (useService) { + BOOL unpromptedElevation; + if (IsUnpromptedElevation(unpromptedElevation)) { + useService = !unpromptedElevation; + } + } + + // Make sure the service registry entries for the instsallation path + // are available. If not don't use the service. + if (useService) { + WCHAR maintenanceServiceKey[MAX_PATH + 1]; + if (CalculateRegistryPathFromFilePath(gInstallDirPath, + maintenanceServiceKey)) { + HKEY baseKey = nullptr; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, + maintenanceServiceKey, 0, + KEY_READ | KEY_WOW64_64KEY, + &baseKey) == ERROR_SUCCESS) { + RegCloseKey(baseKey); + } else { +#ifdef TEST_UPDATER + useService = testOnlyFallbackKeyExists; +#endif + if (!useService) { + lastFallbackError = FALLBACKKEY_NOKEY_ERROR; + } + } + } else { + useService = false; + lastFallbackError = FALLBACKKEY_REGPATH_ERROR; + } + } + + // Originally we used to write "pending" to update.status before + // launching the service command. This is no longer needed now + // since the service command is launched from updater.exe. If anything + // fails in between, we can fall back to using the normal update process + // on our own. + + // If we still want to use the service try to launch the service + // comamnd for the update. + if (useService) { + // If the update couldn't be started, then set useService to false so + // we do the update the old way. + DWORD ret = LaunchServiceSoftwareUpdateCommand(argc, (LPCWSTR *)argv); + useService = (ret == ERROR_SUCCESS); + // If the command was launched then wait for the service to be done. + if (useService) { + bool showProgressUI = false; + // Never show the progress UI when staging updates. + if (!sStagedUpdate) { + // We need to call this separately instead of allowing ShowProgressUI + // to initialize the strings because the service will move the + // ini file out of the way when running updater. + showProgressUI = !InitProgressUIStrings(); + } + + // Wait for the service to stop for 5 seconds. If the service + // has still not stopped then show an indeterminate progress bar. + DWORD lastState = WaitForServiceStop(SVC_NAME, 5); + if (lastState != SERVICE_STOPPED) { + Thread t1; + if (t1.Run(WaitForServiceFinishThread, nullptr) == 0 && + showProgressUI) { + ShowProgressUI(true, false); + } + t1.Join(); + } + + lastState = WaitForServiceStop(SVC_NAME, 1); + if (lastState != SERVICE_STOPPED) { + // If the service doesn't stop after 10 minutes there is + // something seriously wrong. + lastFallbackError = FALLBACKKEY_SERVICE_NO_STOP_ERROR; + useService = false; + } + } else { + lastFallbackError = FALLBACKKEY_LAUNCH_ERROR; + } + } + + // If the service can't be used when staging an update, make sure that + // the UAC prompt is not shown! In this case, just set the status to + // pending and the update will be applied during the next startup. + if (!useService && sStagedUpdate) { + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + WriteStatusFile("pending"); + return 0; + } + + // If we started the service command, and it finished, check the + // update.status file to make sure it succeeded, and if it did + // we need to manually start the PostUpdate process from the + // current user's session of this unelevated updater.exe the + // current process is running as. + // Note that we don't need to do this if we're just staging the update, + // as the PostUpdate step runs when performing the replacing in that case. + if (useService && !sStagedUpdate) { + bool updateStatusSucceeded = false; + if (IsUpdateStatusSucceeded(updateStatusSucceeded) && + updateStatusSucceeded) { + if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) { + fprintf(stderr, "The post update process which runs as the user" + " for service update could not be launched."); + } + } + } + + // If we didn't want to use the service at all, or if an update was + // already happening, or launching the service command failed, then + // launch the elevated updater.exe as we do without the service. + // We don't launch the elevated updater in the case that we did have + // write access all along because in that case the only reason we're + // using the service is because we are testing. + if (!useService && !noServiceFallback && + updateLockFileHandle == INVALID_HANDLE_VALUE) { + SHELLEXECUTEINFO sinfo; + memset(&sinfo, 0, sizeof(SHELLEXECUTEINFO)); + sinfo.cbSize = sizeof(SHELLEXECUTEINFO); + sinfo.fMask = SEE_MASK_FLAG_NO_UI | + SEE_MASK_FLAG_DDEWAIT | + SEE_MASK_NOCLOSEPROCESS; + sinfo.hwnd = nullptr; + sinfo.lpFile = argv[0]; + sinfo.lpParameters = cmdLine; + sinfo.lpVerb = L"runas"; + sinfo.nShow = SW_SHOWNORMAL; + + bool result = ShellExecuteEx(&sinfo); + free(cmdLine); + + if (result) { + WaitForSingleObject(sinfo.hProcess, INFINITE); + CloseHandle(sinfo.hProcess); + } else { + WriteStatusFile(ELEVATION_CANCELED); + } + } + + if (argc > callbackIndex) { + LaunchCallbackApp(argv[5], argc - callbackIndex, + argv + callbackIndex, sUsingService); + } + + CloseHandle(elevatedFileHandle); + + if (!useService && !noServiceFallback && + INVALID_HANDLE_VALUE == updateLockFileHandle) { + // We didn't use the service and we did run the elevated updater.exe. + // The elevated updater.exe is responsible for writing out the + // update.status file. + return 0; + } else if (useService) { + // The service command was launched. The service is responsible for + // writing out the update.status file. + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + return 0; + } else { + // Otherwise the service command was not launched at all. + // We are only reaching this code path because we had write access + // all along to the directory and a fallback key existed, and we + // have fallback disabled (MOZ_NO_SERVICE_FALLBACK env var exists). + // We only currently use this env var from XPCShell tests. + CloseHandle(updateLockFileHandle); + WriteStatusFile(lastFallbackError); + return 0; + } + } + } +#endif + +#if defined(MOZ_WIDGET_GONK) + // In gonk, the master b2g process sets its umask to 0027 because + // there's no reason for it to ever create world-readable files. + // The updater binary, however, needs to do this, and it inherits + // the master process's cautious umask. So we drop down a bit here. + umask(0022); + + // Remount the /system partition as read-write for gonk. The destructor will + // remount /system as read-only. We add an extra level of scope here to avoid + // calling LogFinish() before the GonkAutoMounter destructor has a chance + // to be called + { +#if !defined(TEST_UPDATER) + GonkAutoMounter mounter; + if (mounter.GetAccess() != MountAccess::ReadWrite) { + WriteStatusFile(FILESYSTEM_MOUNT_READWRITE_ERROR); + return 1; + } +#endif +#endif + + if (sStagedUpdate) { + // When staging updates, blow away the old installation directory and create + // it from scratch. + ensure_remove_recursive(gWorkingDirPath); + } + if (!sReplaceRequest) { + // Try to create the destination directory if it doesn't exist + int rv = NS_tmkdir(gWorkingDirPath, 0755); + if (rv != OK && errno != EEXIST) { +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + } + +#ifdef XP_WIN + // For replace requests, we don't need to do any real updates, so this is not + // necessary. + if (!sReplaceRequest) { + // Allocate enough space for the length of the path an optional additional + // trailing slash and null termination. + NS_tchar *destpath = (NS_tchar *) malloc((NS_tstrlen(gWorkingDirPath) + 2) * sizeof(NS_tchar)); + if (!destpath) { + return 1; + } + + NS_tchar *c = destpath; + NS_tstrcpy(c, gWorkingDirPath); + c += NS_tstrlen(gWorkingDirPath); + if (gWorkingDirPath[NS_tstrlen(gWorkingDirPath) - 1] != NS_T('/') && + gWorkingDirPath[NS_tstrlen(gWorkingDirPath) - 1] != NS_T('\\')) { + NS_tstrcat(c, NS_T("/")); + c += NS_tstrlen(NS_T("/")); + } + *c = NS_T('\0'); + c++; + + gDestPath = destpath; + } + + NS_tchar applyDirLongPath[MAXPATHLEN]; + if (!GetLongPathNameW(gWorkingDirPath, applyDirLongPath, + sizeof(applyDirLongPath) / sizeof(applyDirLongPath[0]))) { + LOG(("NS_main: unable to find apply to dir: " LOG_S, gWorkingDirPath)); + LogFinish(); + WriteStatusFile(WRITE_ERROR_APPLY_DIR_PATH); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + if (argc > callbackIndex) { + LaunchCallbackApp(argv[5], argc - callbackIndex, + argv + callbackIndex, sUsingService); + } + return 1; + } + + HANDLE callbackFile = INVALID_HANDLE_VALUE; + if (argc > callbackIndex) { + // If the callback executable is specified it must exist for a successful + // update. It is important we null out the whole buffer here because later + // we make the assumption that the callback application is inside the + // apply-to dir. If we don't have a fully null'ed out buffer it can lead + // to stack corruption which causes crashes and other problems. + NS_tchar callbackLongPath[MAXPATHLEN]; + ZeroMemory(callbackLongPath, sizeof(callbackLongPath)); + NS_tchar *targetPath = argv[callbackIndex]; + NS_tchar buffer[MAXPATHLEN * 2] = { NS_T('\0') }; + size_t bufferLeft = MAXPATHLEN * 2; + if (sReplaceRequest) { + // In case of replace requests, we should look for the callback file in + // the destination directory. + size_t commonPrefixLength = PathCommonPrefixW(argv[callbackIndex], + gInstallDirPath, + nullptr); + NS_tchar *p = buffer; + NS_tstrncpy(p, argv[callbackIndex], commonPrefixLength); + p += commonPrefixLength; + bufferLeft -= commonPrefixLength; + NS_tstrncpy(p, gInstallDirPath + commonPrefixLength, bufferLeft); + + size_t len = NS_tstrlen(gInstallDirPath + commonPrefixLength); + p += len; + bufferLeft -= len; + *p = NS_T('\\'); + ++p; + bufferLeft--; + *p = NS_T('\0'); + NS_tchar installDir[MAXPATHLEN]; + NS_tstrcpy(installDir, gInstallDirPath); + size_t callbackPrefixLength = PathCommonPrefixW(argv[callbackIndex], + installDir, + nullptr); + NS_tstrncpy(p, argv[callbackIndex] + std::max(callbackPrefixLength, + commonPrefixLength), bufferLeft); + targetPath = buffer; + } + if (!GetLongPathNameW(targetPath, callbackLongPath, + sizeof(callbackLongPath)/sizeof(callbackLongPath[0]))) { + LOG(("NS_main: unable to find callback file: " LOG_S, targetPath)); + LogFinish(); + WriteStatusFile(WRITE_ERROR_CALLBACK_PATH); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + if (argc > callbackIndex) { + LaunchCallbackApp(argv[5], + argc - callbackIndex, + argv + callbackIndex, + sUsingService); + } + return 1; + } + + // Doing this is only necessary when we're actually applying a patch. + if (!sReplaceRequest) { + int len = NS_tstrlen(applyDirLongPath); + NS_tchar *s = callbackLongPath; + NS_tchar *d = gCallbackRelPath; + // advance to the apply to directory and advance past the trailing backslash + // if present. + s += len; + if (*s == NS_T('\\')) + ++s; + + // Copy the string and replace backslashes with forward slashes along the + // way. + do { + if (*s == NS_T('\\')) + *d = NS_T('/'); + else + *d = *s; + ++s; + ++d; + } while (*s); + *d = NS_T('\0'); + ++d; + + const size_t callbackBackupPathBufSize = + sizeof(gCallbackBackupPath)/sizeof(gCallbackBackupPath[0]); + const int callbackBackupPathLen = + NS_tsnprintf(gCallbackBackupPath, callbackBackupPathBufSize, + NS_T("%s" CALLBACK_BACKUP_EXT), argv[callbackIndex]); + + if (callbackBackupPathLen < 0 || + callbackBackupPathLen >= static_cast<int>(callbackBackupPathBufSize)) { + LOG(("NS_main: callback backup path truncated")); + LogFinish(); + WriteStatusFile(USAGE_ERROR); + + // Don't attempt to launch the callback when the callback path is + // longer than expected. + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + return 1; + } + + // Make a copy of the callback executable so it can be read when patching. + NS_tremove(gCallbackBackupPath); + if(!CopyFileW(argv[callbackIndex], gCallbackBackupPath, true)) { + DWORD copyFileError = GetLastError(); + LOG(("NS_main: failed to copy callback file " LOG_S + " into place at " LOG_S, argv[callbackIndex], gCallbackBackupPath)); + LogFinish(); + if (copyFileError == ERROR_ACCESS_DENIED) { + WriteStatusFile(WRITE_ERROR_ACCESS_DENIED); + } else { + WriteStatusFile(WRITE_ERROR_CALLBACK_APP); + } + + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + LaunchCallbackApp(argv[callbackIndex], + argc - callbackIndex, + argv + callbackIndex, + sUsingService); + return 1; + } + + // Since the process may be signaled as exited by WaitForSingleObject before + // the release of the executable image try to lock the main executable file + // multiple times before giving up. If we end up giving up, we won't + // fail the update. + const int max_retries = 10; + int retries = 1; + DWORD lastWriteError = 0; + do { + // By opening a file handle wihout FILE_SHARE_READ to the callback + // executable, the OS will prevent launching the process while it is + // being updated. + callbackFile = CreateFileW(targetPath, + DELETE | GENERIC_WRITE, + // allow delete, rename, and write + FILE_SHARE_DELETE | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, 0, nullptr); + if (callbackFile != INVALID_HANDLE_VALUE) + break; + + lastWriteError = GetLastError(); + LOG(("NS_main: callback app file open attempt %d failed. " \ + "File: " LOG_S ". Last error: %d", retries, + targetPath, lastWriteError)); + + Sleep(100); + } while (++retries <= max_retries); + + // CreateFileW will fail if the callback executable is already in use. + if (callbackFile == INVALID_HANDLE_VALUE) { + // Only fail the update if the last error was not a sharing violation. + if (lastWriteError != ERROR_SHARING_VIOLATION) { + LOG(("NS_main: callback app file in use, failed to exclusively open " \ + "executable file: " LOG_S, argv[callbackIndex])); + LogFinish(); + if (lastWriteError == ERROR_ACCESS_DENIED) { + WriteStatusFile(WRITE_ERROR_ACCESS_DENIED); + } else { + WriteStatusFile(WRITE_ERROR_CALLBACK_APP); + } + + NS_tremove(gCallbackBackupPath); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + LaunchCallbackApp(argv[5], + argc - callbackIndex, + argv + callbackIndex, + sUsingService); + return 1; + } + LOG(("NS_main: callback app file in use, continuing without " \ + "exclusive access for executable file: " LOG_S, + argv[callbackIndex])); + } + } + } + + // DELETE_DIR is not required when performing a staged update or replace + // request; it can be used during a replace request but then it doesn't + // use gDeleteDirPath. + if (!sStagedUpdate && !sReplaceRequest) { + // The directory to move files that are in use to on Windows. This directory + // will be deleted after the update is finished, on OS reboot using + // MoveFileEx if it contains files that are in use, or by the post update + // process after the update finishes. On Windows when performing a normal + // update (e.g. the update is not a staged update and is not a replace + // request) gWorkingDirPath is the same as gInstallDirPath and + // gWorkingDirPath is used because it is the destination directory. + NS_tsnprintf(gDeleteDirPath, + sizeof(gDeleteDirPath) / sizeof(gDeleteDirPath[0]), + NS_T("%s/%s"), gWorkingDirPath, DELETE_DIR); + + if (NS_taccess(gDeleteDirPath, F_OK)) { + NS_tmkdir(gDeleteDirPath, 0755); + } + } +#endif /* XP_WIN */ + + // Run update process on a background thread. ShowProgressUI may return + // before QuitProgressUI has been called, so wait for UpdateThreadFunc to + // terminate. Avoid showing the progress UI when staging an update, or if this + // is an elevated process on OSX. + Thread t; + if (t.Run(UpdateThreadFunc, nullptr) == 0) { + if (!sStagedUpdate && !sReplaceRequest +#ifdef XP_MACOSX + && !isElevated +#endif + ) { + ShowProgressUI(); + } + } + t.Join(); + +#ifdef XP_WIN + if (argc > callbackIndex && !sReplaceRequest) { + if (callbackFile != INVALID_HANDLE_VALUE) { + CloseHandle(callbackFile); + } + // Remove the copy of the callback executable. + NS_tremove(gCallbackBackupPath); + } + + if (!sStagedUpdate && !sReplaceRequest && _wrmdir(gDeleteDirPath)) { + LOG(("NS_main: unable to remove directory: " LOG_S ", err: %d", + DELETE_DIR, errno)); + // The directory probably couldn't be removed due to it containing files + // that are in use and will be removed on OS reboot. The call to remove the + // directory on OS reboot is done after the calls to remove the files so the + // files are removed first on OS reboot since the directory must be empty + // for the directory removal to be successful. The MoveFileEx call to remove + // the directory on OS reboot will fail if the process doesn't have write + // access to the HKEY_LOCAL_MACHINE registry key but this is ok since the + // installer / uninstaller will delete the directory along with its contents + // after an update is applied, on reinstall, and on uninstall. + if (MoveFileEx(gDeleteDirPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG(("NS_main: directory will be removed on OS reboot: " LOG_S, + DELETE_DIR)); + } else { + LOG(("NS_main: failed to schedule OS reboot removal of " \ + "directory: " LOG_S, DELETE_DIR)); + } + } +#endif /* XP_WIN */ + +#if defined(MOZ_WIDGET_GONK) + } // end the extra level of scope for the GonkAutoMounter +#endif + +#ifdef XP_MACOSX + // When the update is successful remove the precomplete file in the root of + // the application bundle and move the distribution directory from + // Contents/MacOS to Contents/Resources and if both exist delete the + // directory under Contents/MacOS (see Bug 1068439). + if (gSucceeded && !sStagedUpdate) { + NS_tchar oldPrecomplete[MAXPATHLEN]; + NS_tsnprintf(oldPrecomplete, sizeof(oldPrecomplete)/sizeof(oldPrecomplete[0]), + NS_T("%s/precomplete"), gInstallDirPath); + NS_tremove(oldPrecomplete); + + NS_tchar oldDistDir[MAXPATHLEN]; + NS_tsnprintf(oldDistDir, sizeof(oldDistDir)/sizeof(oldDistDir[0]), + NS_T("%s/Contents/MacOS/distribution"), gInstallDirPath); + int rv = NS_taccess(oldDistDir, F_OK); + if (!rv) { + NS_tchar newDistDir[MAXPATHLEN]; + NS_tsnprintf(newDistDir, sizeof(newDistDir)/sizeof(newDistDir[0]), + NS_T("%s/Contents/Resources/distribution"), gInstallDirPath); + rv = NS_taccess(newDistDir, F_OK); + if (!rv) { + LOG(("New distribution directory already exists... removing old " \ + "distribution directory: " LOG_S, oldDistDir)); + rv = ensure_remove_recursive(oldDistDir); + if (rv) { + LOG(("Removing old distribution directory failed - err: %d", rv)); + } + } else { + LOG(("Moving old distribution directory to new location. src: " LOG_S \ + ", dst:" LOG_S, oldDistDir, newDistDir)); + rv = rename_file(oldDistDir, newDistDir, true); + if (rv) { + LOG(("Moving old distribution directory to new location failed - " \ + "err: %d", rv)); + } + } + } + } + + if (isElevated) { + SetGroupOwnershipAndPermissions(gInstallDirPath); + freeArguments(argc, argv); + CleanupElevatedMacUpdate(false); + } else if (IsOwnedByGroupAdmin(gInstallDirPath)) { + // If the group ownership of the Firefox .app bundle was set to the "admin" + // group during a previous elevated update, we need to ensure that all files + // in the bundle have group ownership of "admin" as well as write permission + // for the group to not break updates in the future. + SetGroupOwnershipAndPermissions(gInstallDirPath); + } +#endif /* XP_MACOSX */ + + LogFinish(); + + int retVal = LaunchCallbackAndPostProcessApps(argc, argv, callbackIndex +#ifdef XP_WIN + , elevatedLockFilePath + , updateLockFileHandle +#elif XP_MACOSX + , isElevated +#endif + ); + + return retVal ? retVal : (gSucceeded ? 0 : 1); +} + +class ActionList +{ +public: + ActionList() : mFirst(nullptr), mLast(nullptr), mCount(0) { } + ~ActionList(); + + void Append(Action* action); + int Prepare(); + int Execute(); + void Finish(int status); + +private: + Action *mFirst; + Action *mLast; + int mCount; +}; + +ActionList::~ActionList() +{ + Action* a = mFirst; + while (a) { + Action *b = a; + a = a->mNext; + delete b; + } +} + +void +ActionList::Append(Action *action) +{ + if (mLast) + mLast->mNext = action; + else + mFirst = action; + + mLast = action; + mCount++; +} + +int +ActionList::Prepare() +{ + // If the action list is empty then we should fail in order to signal that + // something has gone wrong. Otherwise we report success when nothing is + // actually done. See bug 327140. + if (mCount == 0) { + LOG(("empty action list")); + return MAR_ERROR_EMPTY_ACTION_LIST; + } + + Action *a = mFirst; + int i = 0; + while (a) { + int rv = a->Prepare(); + if (rv) + return rv; + + float percent = float(++i) / float(mCount); + UpdateProgressUI(PROGRESS_PREPARE_SIZE * percent); + + a = a->mNext; + } + + return OK; +} + +int +ActionList::Execute() +{ + int currentProgress = 0, maxProgress = 0; + Action *a = mFirst; + while (a) { + maxProgress += a->mProgressCost; + a = a->mNext; + } + + a = mFirst; + while (a) { + int rv = a->Execute(); + if (rv) { + LOG(("### execution failed")); + return rv; + } + + currentProgress += a->mProgressCost; + float percent = float(currentProgress) / float(maxProgress); + UpdateProgressUI(PROGRESS_PREPARE_SIZE + + PROGRESS_EXECUTE_SIZE * percent); + + a = a->mNext; + } + + return OK; +} + +void +ActionList::Finish(int status) +{ + Action *a = mFirst; + int i = 0; + while (a) { + a->Finish(status); + + float percent = float(++i) / float(mCount); + UpdateProgressUI(PROGRESS_PREPARE_SIZE + + PROGRESS_EXECUTE_SIZE + + PROGRESS_FINISH_SIZE * percent); + + a = a->mNext; + } + + if (status == OK) + gSucceeded = true; +} + + +#ifdef XP_WIN +int add_dir_entries(const NS_tchar *dirpath, ActionList *list) +{ + int rv = OK; + WIN32_FIND_DATAW finddata; + HANDLE hFindFile; + NS_tchar searchspec[MAXPATHLEN]; + NS_tchar foundpath[MAXPATHLEN]; + + NS_tsnprintf(searchspec, sizeof(searchspec)/sizeof(searchspec[0]), + NS_T("%s*"), dirpath); + mozilla::UniquePtr<const NS_tchar> pszSpec(get_full_path(searchspec)); + + hFindFile = FindFirstFileW(pszSpec.get(), &finddata); + if (hFindFile != INVALID_HANDLE_VALUE) { + do { + // Don't process the current or parent directory. + if (NS_tstrcmp(finddata.cFileName, NS_T(".")) == 0 || + NS_tstrcmp(finddata.cFileName, NS_T("..")) == 0) + continue; + + NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]), + NS_T("%s%s"), dirpath, finddata.cFileName); + if (finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]), + NS_T("%s/"), foundpath); + // Recurse into the directory. + rv = add_dir_entries(foundpath, list); + if (rv) { + LOG(("add_dir_entries error: " LOG_S ", err: %d", foundpath, rv)); + return rv; + } + } else { + // Add the file to be removed to the ActionList. + NS_tchar *quotedpath = get_quoted_path(foundpath); + if (!quotedpath) + return PARSE_ERROR; + + Action *action = new RemoveFile(); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on recurse: " LOG_S ", err: %d", + quotedpath, rv)); + return rv; + } + free(quotedpath); + + list->Append(action); + } + } while (FindNextFileW(hFindFile, &finddata) != 0); + + FindClose(hFindFile); + { + // Add the directory to be removed to the ActionList. + NS_tchar *quotedpath = get_quoted_path(dirpath); + if (!quotedpath) + return PARSE_ERROR; + + Action *action = new RemoveDir(); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on close: " LOG_S ", err: %d", + quotedpath, rv)); + } else { + list->Append(action); + } + free(quotedpath); + } + } + + return rv; +} + +#elif defined(SOLARIS) +int add_dir_entries(const NS_tchar *dirpath, ActionList *list) +{ + int rv = OK; + NS_tchar foundpath[MAXPATHLEN]; + struct { + dirent dent_buffer; + char chars[MAXNAMLEN]; + } ent_buf; + struct dirent* ent; + mozilla::UniquePtr<NS_tchar[]> searchpath(get_full_path(dirpath)); + + DIR* dir = opendir(searchpath.get()); + if (!dir) { + LOG(("add_dir_entries error on opendir: " LOG_S ", err: %d", searchpath.get(), + errno)); + return UNEXPECTED_FILE_OPERATION_ERROR; + } + + while (readdir_r(dir, (dirent *)&ent_buf, &ent) == 0 && ent) { + if ((strcmp(ent->d_name, ".") == 0) || + (strcmp(ent->d_name, "..") == 0)) + continue; + + NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]), + NS_T("%s%s"), searchpath.get(), ent->d_name); + struct stat64 st_buf; + int test = stat64(foundpath, &st_buf); + if (test) { + closedir(dir); + return UNEXPECTED_FILE_OPERATION_ERROR; + } + if (S_ISDIR(st_buf.st_mode)) { + NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]), + NS_T("%s/"), foundpath); + // Recurse into the directory. + rv = add_dir_entries(foundpath, list); + if (rv) { + LOG(("add_dir_entries error: " LOG_S ", err: %d", foundpath, rv)); + closedir(dir); + return rv; + } + } else { + // Add the file to be removed to the ActionList. + NS_tchar *quotedpath = get_quoted_path(get_relative_path(foundpath)); + if (!quotedpath) { + closedir(dir); + return PARSE_ERROR; + } + + Action *action = new RemoveFile(); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on recurse: " LOG_S ", err: %d", + quotedpath, rv)); + closedir(dir); + return rv; + } + + list->Append(action); + } + } + closedir(dir); + + // Add the directory to be removed to the ActionList. + NS_tchar *quotedpath = get_quoted_path(get_relative_path(dirpath)); + if (!quotedpath) + return PARSE_ERROR; + + Action *action = new RemoveDir(); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on close: " LOG_S ", err: %d", + quotedpath, rv)); + } + else { + list->Append(action); + } + + return rv; +} + +#else + +int add_dir_entries(const NS_tchar *dirpath, ActionList *list) +{ + int rv = OK; + FTS *ftsdir; + FTSENT *ftsdirEntry; + mozilla::UniquePtr<NS_tchar[]> searchpath(get_full_path(dirpath)); + + // Remove the trailing slash so the paths don't contain double slashes. The + // existence of the slash has already been checked in DoUpdate. + searchpath[NS_tstrlen(searchpath.get()) - 1] = NS_T('\0'); + char* const pathargv[] = {searchpath.get(), nullptr}; + + // FTS_NOCHDIR is used so relative paths from the destination directory are + // returned. + if (!(ftsdir = fts_open(pathargv, + FTS_PHYSICAL | FTS_NOSTAT | FTS_XDEV | FTS_NOCHDIR, + nullptr))) + return UNEXPECTED_FILE_OPERATION_ERROR; + + while ((ftsdirEntry = fts_read(ftsdir)) != nullptr) { + NS_tchar foundpath[MAXPATHLEN]; + NS_tchar *quotedpath = nullptr; + Action *action = nullptr; + + switch (ftsdirEntry->fts_info) { + // Filesystem objects that shouldn't be in the application's directories + case FTS_SL: + case FTS_SLNONE: + case FTS_DEFAULT: + LOG(("add_dir_entries: found a non-standard file: " LOG_S, + ftsdirEntry->fts_path)); + // Fall through and try to remove as a file + MOZ_FALLTHROUGH; + + // Files + case FTS_F: + case FTS_NSOK: + // Add the file to be removed to the ActionList. + NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]), + NS_T("%s"), ftsdirEntry->fts_accpath); + quotedpath = get_quoted_path(get_relative_path(foundpath)); + if (!quotedpath) { + rv = UPDATER_QUOTED_PATH_MEM_ERROR; + break; + } + action = new RemoveFile(); + rv = action->Parse(quotedpath); + free(quotedpath); + if (!rv) + list->Append(action); + break; + + // Directories + case FTS_DP: + rv = OK; + // Add the directory to be removed to the ActionList. + NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]), + NS_T("%s/"), ftsdirEntry->fts_accpath); + quotedpath = get_quoted_path(get_relative_path(foundpath)); + if (!quotedpath) { + rv = UPDATER_QUOTED_PATH_MEM_ERROR; + break; + } + + action = new RemoveDir(); + rv = action->Parse(quotedpath); + free(quotedpath); + if (!rv) + list->Append(action); + break; + + // Errors + case FTS_DNR: + case FTS_NS: + // ENOENT is an acceptable error for FTS_DNR and FTS_NS and means that + // we're racing with ourselves. Though strange, the entry will be + // removed anyway. + if (ENOENT == ftsdirEntry->fts_errno) { + rv = OK; + break; + } + MOZ_FALLTHROUGH; + + case FTS_ERR: + rv = UNEXPECTED_FILE_OPERATION_ERROR; + LOG(("add_dir_entries: fts_read() error: " LOG_S ", err: %d", + ftsdirEntry->fts_path, ftsdirEntry->fts_errno)); + break; + + case FTS_DC: + rv = UNEXPECTED_FILE_OPERATION_ERROR; + LOG(("add_dir_entries: fts_read() returned FT_DC: " LOG_S, + ftsdirEntry->fts_path)); + break; + + default: + // FTS_D is ignored and FTS_DP is used instead (post-order). + rv = OK; + break; + } + + if (rv != OK) + break; + } + + fts_close(ftsdir); + + return rv; +} +#endif + +static NS_tchar* +GetManifestContents(const NS_tchar *manifest) +{ + AutoFile mfile(NS_tfopen(manifest, NS_T("rb"))); + if (mfile == nullptr) { + LOG(("GetManifestContents: error opening manifest file: " LOG_S, manifest)); + return nullptr; + } + + struct stat ms; + int rv = fstat(fileno((FILE *)mfile), &ms); + if (rv) { + LOG(("GetManifestContents: error stating manifest file: " LOG_S, manifest)); + return nullptr; + } + + char *mbuf = (char *) malloc(ms.st_size + 1); + if (!mbuf) + return nullptr; + + size_t r = ms.st_size; + char *rb = mbuf; + while (r) { + const size_t count = mmin(SSIZE_MAX, r); + size_t c = fread(rb, 1, count, mfile); + if (c != count) { + LOG(("GetManifestContents: error reading manifest file: " LOG_S, manifest)); + free(mbuf); + return nullptr; + } + + r -= c; + rb += c; + } + mbuf[ms.st_size] = '\0'; + rb = mbuf; + +#ifndef XP_WIN + return rb; +#else + NS_tchar *wrb = (NS_tchar *) malloc((ms.st_size + 1) * sizeof(NS_tchar)); + if (!wrb) { + free(mbuf); + return nullptr; + } + + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, rb, -1, wrb, + ms.st_size + 1)) { + LOG(("GetManifestContents: error converting utf8 to utf16le: %d", GetLastError())); + free(mbuf); + free(wrb); + return nullptr; + } + free(mbuf); + + return wrb; +#endif +} + +int AddPreCompleteActions(ActionList *list) +{ + if (sIsOSUpdate) { + return OK; + } + +#ifdef XP_MACOSX + mozilla::UniquePtr<NS_tchar[]> manifestPath(get_full_path( + NS_T("Contents/Resources/precomplete"))); +#else + mozilla::UniquePtr<NS_tchar[]> manifestPath(get_full_path( + NS_T("precomplete"))); +#endif + + NS_tchar *rb = GetManifestContents(manifestPath.get()); + if (rb == nullptr) { + LOG(("AddPreCompleteActions: error getting contents of precomplete " \ + "manifest")); + // Applications aren't required to have a precomplete manifest. The mar + // generation scripts enforce the presence of a precomplete manifest. + return OK; + } + + int rv; + NS_tchar *line; + while((line = mstrtok(kNL, &rb)) != 0) { + // skip comments + if (*line == NS_T('#')) + continue; + + NS_tchar *token = mstrtok(kWhitespace, &line); + if (!token) { + LOG(("AddPreCompleteActions: token not found in manifest")); + return PARSE_ERROR; + } + + Action *action = nullptr; + if (NS_tstrcmp(token, NS_T("remove")) == 0) { // rm file + action = new RemoveFile(); + } + else if (NS_tstrcmp(token, NS_T("remove-cc")) == 0) { // no longer supported + continue; + } + else if (NS_tstrcmp(token, NS_T("rmdir")) == 0) { // rmdir if empty + action = new RemoveDir(); + } + else { + LOG(("AddPreCompleteActions: unknown token: " LOG_S, token)); + return PARSE_ERROR; + } + + if (!action) + return BAD_ACTION_ERROR; + + rv = action->Parse(line); + if (rv) + return rv; + + list->Append(action); + } + + return OK; +} + +int DoUpdate() +{ + NS_tchar manifest[MAXPATHLEN]; + NS_tsnprintf(manifest, sizeof(manifest)/sizeof(manifest[0]), + NS_T("%s/updating/update.manifest"), gWorkingDirPath); + ensure_parent_dir(manifest); + + // extract the manifest + int rv = gArchiveReader.ExtractFile("updatev3.manifest", manifest); + if (rv) { + rv = gArchiveReader.ExtractFile("updatev2.manifest", manifest); + if (rv) { + LOG(("DoUpdate: error extracting manifest file")); + return rv; + } + } + + NS_tchar *rb = GetManifestContents(manifest); + NS_tremove(manifest); + if (rb == nullptr) { + LOG(("DoUpdate: error opening manifest file: " LOG_S, manifest)); + return READ_ERROR; + } + + + ActionList list; + NS_tchar *line; + bool isFirstAction = true; + + while((line = mstrtok(kNL, &rb)) != 0) { + // skip comments + if (*line == NS_T('#')) + continue; + + NS_tchar *token = mstrtok(kWhitespace, &line); + if (!token) { + LOG(("DoUpdate: token not found in manifest")); + return PARSE_ERROR; + } + + if (isFirstAction) { + isFirstAction = false; + // The update manifest isn't required to have a type declaration. The mar + // generation scripts enforce the presence of the type declaration. + if (NS_tstrcmp(token, NS_T("type")) == 0) { + const NS_tchar *type = mstrtok(kQuote, &line); + LOG(("UPDATE TYPE " LOG_S, type)); + if (NS_tstrcmp(type, NS_T("complete")) == 0) { + rv = AddPreCompleteActions(&list); + if (rv) + return rv; + } + continue; + } + } + + Action *action = nullptr; + if (NS_tstrcmp(token, NS_T("remove")) == 0) { // rm file + action = new RemoveFile(); + } + else if (NS_tstrcmp(token, NS_T("rmdir")) == 0) { // rmdir if empty + action = new RemoveDir(); + } + else if (NS_tstrcmp(token, NS_T("rmrfdir")) == 0) { // rmdir recursive + const NS_tchar *reldirpath = mstrtok(kQuote, &line); + if (!reldirpath) + return PARSE_ERROR; + + if (reldirpath[NS_tstrlen(reldirpath) - 1] != NS_T('/')) + return PARSE_ERROR; + + rv = add_dir_entries(reldirpath, &list); + if (rv) + return rv; + + continue; + } + else if (NS_tstrcmp(token, NS_T("add")) == 0) { + action = new AddFile(); + } + else if (NS_tstrcmp(token, NS_T("patch")) == 0) { + action = new PatchFile(); + } + else if (NS_tstrcmp(token, NS_T("add-if")) == 0) { // Add if exists + action = new AddIfFile(); + } + else if (NS_tstrcmp(token, NS_T("add-if-not")) == 0) { // Add if not exists + action = new AddIfNotFile(); + } + else if (NS_tstrcmp(token, NS_T("patch-if")) == 0) { // Patch if exists + action = new PatchIfFile(); + } + else { + LOG(("DoUpdate: unknown token: " LOG_S, token)); + return PARSE_ERROR; + } + + if (!action) + return BAD_ACTION_ERROR; + + rv = action->Parse(line); + if (rv) + return rv; + + list.Append(action); + } + + rv = list.Prepare(); + if (rv) + return rv; + + rv = list.Execute(); + + list.Finish(rv); + return rv; +} diff --git a/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest b/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest new file mode 100644 index 000000000..9a6cdb565 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="Updater" + type="win32" +/> +<description>Updater</description> +<dependency> + <dependentAssembly> + <assemblyIdentity + type="win32" + name="Microsoft.Windows.Common-Controls" + version="6.0.0.0" + processorArchitecture="*" + publicKeyToken="6595b64144ccf1df" + language="*" + /> + </dependentAssembly> +</dependency> +<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:security> + <ms_asmv3:requestedPrivileges> + <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" /> + </ms_asmv3:requestedPrivileges> + </ms_asmv3:security> +</ms_asmv3:trustInfo> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> + </application> + </compatibility> +</assembly> diff --git a/toolkit/mozapps/update/updater/updater.exe.manifest b/toolkit/mozapps/update/updater/updater.exe.manifest new file mode 100644 index 000000000..cd229c954 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.exe.manifest @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="Updater" + type="win32" +/> +<description>Updater</description> +<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:security> + <ms_asmv3:requestedPrivileges> + <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" /> + </ms_asmv3:requestedPrivileges> + </ms_asmv3:security> +</ms_asmv3:trustInfo> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> + </application> + </compatibility> +</assembly> diff --git a/toolkit/mozapps/update/updater/updater.ico b/toolkit/mozapps/update/updater/updater.ico Binary files differnew file mode 100644 index 000000000..48457029d --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.ico diff --git a/toolkit/mozapps/update/updater/updater.png b/toolkit/mozapps/update/updater/updater.png Binary files differnew file mode 100644 index 000000000..7b5e78907 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.png diff --git a/toolkit/mozapps/update/updater/updater.rc b/toolkit/mozapps/update/updater/updater.rc new file mode 100644 index 000000000..7603eecb6 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.rc @@ -0,0 +1,137 @@ +/* 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/. */ + +// Microsoft Visual C++ generated resource script. +// +#ifdef TEST_UPDATER +#include "../resource.h" +#define MANIFEST_PATH "../updater.exe.manifest" +#define COMCTL32_MANIFEST_PATH "../updater.exe.comctl32.manifest" +#define ICON_PATH "../updater.ico" +#else +#include "resource.h" +#define MANIFEST_PATH "updater.exe.manifest" +#define COMCTL32_MANIFEST_PATH "updater.exe.comctl32.manifest" +#define ICON_PATH "updater.ico" +#endif + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winresrc.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// RT_MANIFEST +// + +1 RT_MANIFEST MANIFEST_PATH +IDR_COMCTL32_MANIFEST RT_MANIFEST COMCTL32_MANIFEST_PATH + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +IDI_DIALOG ICON ICON_PATH + + +///////////////////////////////////////////////////////////////////////////// +// +// Embedded an identifier to uniquely identiy this as a Mozilla updater. +// + +STRINGTABLE +{ + IDS_UPDATER_IDENTITY, "moz-updater.exe-4cdccec4-5ee0-4a06-9817-4cd899a9db49" +} + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_DIALOG DIALOGEX 0, 0, 253, 41 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "",IDC_PROGRESS,"msctls_progress32",WS_BORDER,7,24,239,10 + LTEXT "",IDC_INFO,7,8,239,13,SS_NOPREFIX +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_DIALOG, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 246 + TOPMARGIN, 7 + BOTTOMMARGIN, 39 + END +END +#endif // APSTUDIO_INVOKED + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winresrc.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/toolkit/mozapps/update/updater/win_dirent.cpp b/toolkit/mozapps/update/updater/win_dirent.cpp new file mode 100644 index 000000000..b0807ba5e --- /dev/null +++ b/toolkit/mozapps/update/updater/win_dirent.cpp @@ -0,0 +1,78 @@ +/* -*- 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 "win_dirent.h" +#include <errno.h> +#include <string.h> + +// This file implements the minimum set of dirent APIs used by updater.cpp on +// Windows. If updater.cpp is modified to use more of this API, we need to +// implement those parts here too. + +static dirent gDirEnt; + +DIR::DIR(const WCHAR* path) + : findHandle(INVALID_HANDLE_VALUE) +{ + memset(name, 0, sizeof(name)); + wcsncpy(name, path, sizeof(name)/sizeof(name[0])); + wcsncat(name, L"\\*", sizeof(name)/sizeof(name[0]) - wcslen(name) - 1); +} + +DIR::~DIR() +{ + if (findHandle != INVALID_HANDLE_VALUE) { + FindClose(findHandle); + } +} + +dirent::dirent() +{ + d_name[0] = L'\0'; +} + +DIR* +opendir(const WCHAR* path) +{ + return new DIR(path); +} + +int +closedir(DIR* dir) +{ + delete dir; + return 0; +} + +dirent* readdir(DIR* dir) +{ + WIN32_FIND_DATAW data; + if (dir->findHandle != INVALID_HANDLE_VALUE) { + BOOL result = FindNextFileW(dir->findHandle, &data); + if (!result) { + if (GetLastError() != ERROR_FILE_NOT_FOUND) { + errno = ENOENT; + } + return 0; + } + } else { + // Reading the first directory entry + dir->findHandle = FindFirstFileW(dir->name, &data); + if (dir->findHandle == INVALID_HANDLE_VALUE) { + if (GetLastError() == ERROR_FILE_NOT_FOUND) { + errno = ENOENT; + } else { + errno = EBADF; + } + return 0; + } + } + memset(gDirEnt.d_name, 0, sizeof(gDirEnt.d_name)); + wcsncpy(gDirEnt.d_name, data.cFileName, + sizeof(gDirEnt.d_name)/sizeof(gDirEnt.d_name[0])); + return &gDirEnt; +} + diff --git a/toolkit/mozapps/update/updater/xpcshellCertificate.der b/toolkit/mozapps/update/updater/xpcshellCertificate.der Binary files differnew file mode 100644 index 000000000..185b2dff4 --- /dev/null +++ b/toolkit/mozapps/update/updater/xpcshellCertificate.der |