1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983
|
/* 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, mochitests-chrome, and mochitest-browser-chrome. */
// Definitions needed to run eslint on this file.
/* global AppConstants, DATA_URI_SPEC, LOG_FUNCTION */
/* global Services, URL_HOST, TestUtils */
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
ChromeUtils.defineESModuleGetters(this, {
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
ctypes: "resource://gre/modules/ctypes.sys.mjs",
TelemetryArchiveTesting:
"resource://testing-common/TelemetryArchiveTesting.sys.mjs",
});
const PREF_APP_UPDATE_AUTO = "app.update.auto";
const PREF_APP_UPDATE_BACKGROUND_ALLOWDOWNLOADSWITHOUTBITS =
"app.update.background.allowDownloadsWithoutBITS";
const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
const PREF_APP_UPDATE_BADGEWAITTIME = "app.update.badgeWaitTime";
const PREF_APP_UPDATE_BITS_ENABLED = "app.update.BITS.enabled";
const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations";
const PREF_APP_UPDATE_CHANNEL = "app.update.channel";
const PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS = "app.update.download.maxAttempts";
const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS = "app.update.download.attempts";
const PREF_APP_UPDATE_DISABLEDFORTESTING = "app.update.disabledForTesting";
const PREF_APP_UPDATE_INTERVAL = "app.update.interval";
const PREF_APP_UPDATE_LASTUPDATETIME =
"app.update.lastUpdateTime.background-update-timer";
const PREF_APP_UPDATE_LOG = "app.update.log";
const PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD = "app.update.notifyDuringDownload";
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_SOCKET_MAXERRORS = "app.update.socket.maxErrors";
const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled";
const PREF_APP_UPDATE_UNSUPPORTED_URL = "app.update.unsupported.url";
const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details";
const PREF_APP_UPDATE_URL_MANUAL = "app.update.url.manual";
const PREF_APP_UPDATE_LANGPACK_ENABLED = "app.update.langpack.enabled";
const PREFBRANCH_APP_PARTNER = "app.partner.";
const PREF_DISTRIBUTION_ID = "distribution.id";
const PREF_DISTRIBUTION_VERSION = "distribution.version";
const CONFIG_APP_UPDATE_AUTO = "app.update.auto";
const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
const NS_APP_USER_PROFILE_50_DIR = "ProfD";
const NS_GRE_BIN_DIR = "GreBinD";
const NS_GRE_DIR = "GreD";
const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
const XRE_EXECUTABLE_FILE = "XREExeF";
const XRE_OLD_UPDATE_ROOT_DIR = "OldUpdRootD";
const XRE_UPDATE_ROOT_DIR = "UpdRootD";
const DIR_PATCH = "0";
const DIR_TOBEDELETED = "tobedeleted";
const DIR_UPDATES = "updates";
const DIR_UPDATED =
AppConstants.platform == "macosx" ? "Updated.app" : "updated";
const DIR_DOWNLOADING = "downloading";
const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
const FILE_ACTIVE_UPDATE_XML_TMP = "active-update.xml.tmp";
const FILE_APPLICATION_INI = "application.ini";
const FILE_BACKUP_UPDATE_CONFIG_JSON = "backup-update-config.json";
const FILE_BACKUP_UPDATE_ELEVATED_LOG = "backup-update-elevated.log";
const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
const FILE_CHANNEL_PREFS =
AppConstants.platform == "macosx" ? "ChannelPrefs" : "channel-prefs.js";
const FILE_INFO_PLIST = "Info.plist";
const FILE_LAST_UPDATE_ELEVATED_LOG = "last-update-elevated.log";
const FILE_LAST_UPDATE_LOG = "last-update.log";
const FILE_PRECOMPLETE = "precomplete";
const FILE_PRECOMPLETE_BAK = "precomplete.bak";
const FILE_TEST_PROCESS_UPDATES = "test_process_updates.txt";
const FILE_UPDATE_CONFIG_JSON = "update-config.json";
const FILE_UPDATE_ELEVATED_LOG = "update-elevated.log";
const FILE_UPDATE_LOG = "update.log";
const FILE_UPDATE_MAR = "update.mar";
const FILE_UPDATE_SETTINGS_FRAMEWORK = "UpdateSettings";
const FILE_UPDATE_SETTINGS_INI = "update-settings.ini";
const FILE_UPDATE_SETTINGS_INI_BAK = "update-settings.ini.bak";
const FILE_UPDATE_STATUS = "update.status";
const FILE_UPDATE_TEST = "update.test";
const FILE_UPDATE_VERSION = "update.version";
const FILE_UPDATER_INI = "updater.ini";
const FILE_UPDATES_XML = "updates.xml";
const FILE_UPDATES_XML_TMP = "updates.xml.tmp";
const UPDATE_SETTINGS_CONTENTS =
"[Settings]\nACCEPTED_MAR_CHANNEL_IDS=xpcshell-test\n";
const PRECOMPLETE_CONTENTS = 'rmdir "nonexistent_dir/"\n';
const DIR_APP_INFO_PLIST_FILE_CONTENTS = `<?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>CFBundleDisplayName</key><string>dir</string>
<key>CFBundleExecutable</key><string>${AppConstants.MOZ_APP_NAME}</string>
<key>CFBundleIdentifier</key><string>${AppConstants.MOZ_MACBUNDLE_ID}</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>dir</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleSignature</key><string>????</string>
<key>CFBundleVersion</key><string>1.0</string>
</dict>
</plist>`;
const PR_RDWR = 0x04;
const PR_CREATE_FILE = 0x08;
const PR_TRUNCATE = 0x20;
var gChannel;
var gDebugTest = false;
/* import-globals-from 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);
ChromeUtils.defineLazyGetter(this, "gAUS", function test_gAUS() {
return Cc["@mozilla.org/updates/update-service;1"]
.getService(Ci.nsIApplicationUpdateService)
.QueryInterface(Ci.nsITimerCallback)
.QueryInterface(Ci.nsIObserver);
});
XPCOMUtils.defineLazyServiceGetter(
this,
"gUpdateManager",
"@mozilla.org/updates/update-manager;1",
Ci.nsIUpdateManager
);
XPCOMUtils.defineLazyServiceGetter(
this,
"gUpdateChecker",
"@mozilla.org/updates/update-checker;1",
Ci.nsIUpdateChecker
);
ChromeUtils.defineLazyGetter(this, "gDefaultPrefBranch", function test_gDPB() {
return Services.prefs.getDefaultBranch(null);
});
ChromeUtils.defineLazyGetter(this, "gPrefRoot", function test_gPR() {
return Services.prefs.getBranch(null);
});
/**
* Waits for the specified topic and (optionally) status.
*
* @param topic
* String representing the topic to wait for.
* @param status (optional)
* A string representing the status on said topic to wait for.
* @return A promise which will resolve the first time an event occurs on the
* specified topic, and (optionally) with the specified status.
*/
function waitForEvent(topic, status = null) {
return new Promise(resolve =>
Services.obs.addObserver(
{
observe(subject, innerTopic, innerStatus) {
if (!status || status == innerStatus) {
Services.obs.removeObserver(this, topic);
resolve(innerStatus);
}
},
},
topic
)
);
}
/* Triggers post-update processing */
async function testPostUpdateProcessing() {
// All of the post update processing happens during initialization
await gAUS.internal.init(/* force = */ true);
}
/* Initializes the update service stub */
async function initUpdateServiceStub() {
const updateServiceStub = Cc[
"@mozilla.org/updates/update-service-stub;1"
].getService(Ci.nsIApplicationUpdateServiceStub);
await updateServiceStub.init();
}
/* Initializes the Update Service, even if it has already been initialized */
async function reInitUpdateService() {
return gAUS.internal.init(/* force = */ true);
}
async function initUpdateService() {
return gAUS.init();
}
/**
* Reloads the update xml files.
*
* @param skipFiles (optional)
* If true, the update xml files will not be read and the metadata will
* be reset. If false (the default), the update xml files will be read
* to populate the update metadata.
*/
async function reloadUpdateManagerData(skipFiles = false) {
await gUpdateManager.internal.reload(skipFiles);
}
const observer = {
observe(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: ChromeUtils.generateQI(["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.unlockPref(PREF_APP_UPDATE_CHANNEL);
gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, gChannel);
gPrefRoot.addObserver(PREF_APP_UPDATE_CHANNEL, observer);
}
/**
* Sets the effective update url.
*
* @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 update URL to " + url);
// The Update URL is stored in appinfo. We can replace this process's appinfo
// directly, but that will affect only this process. Luckily, the update URL
// is only ever read from the update process. This means that replacing
// Services.appinfo is sufficient and we don't need to worry about registering
// a replacement factory or anything like that.
let origAppInfo = Services.appinfo;
registerCleanupFunction(() => {
Services.appinfo = origAppInfo;
});
// Override the appinfo object with an object that exposes all of the same
// properties overriding just the updateURL.
let mockAppInfo = Object.create(origAppInfo, {
updateURL: {
configurable: true,
enumerable: true,
writable: false,
value: url,
},
});
Services.appinfo = mockAppInfo;
}
/**
* 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) {
let file = getUpdateDirFile(
aIsActiveUpdate ? FILE_ACTIVE_UPDATE_XML : FILE_UPDATES_XML
);
writeFile(file, 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 = getUpdateDirFile(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 = getUpdateDirFile(FILE_UPDATE_VERSION);
writeFile(file, aVersion + "\n");
}
/**
* 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.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
}
fos.init(aFile, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, PERMS_FILE, 0);
fos.write(aText, aText.length);
fos.close();
}
/**
* Attempts to remove a file. Does not fail if the file does not exist.
*
* @param file
* The `nsIFile` to remove.
*/
function ensureRemoved(file) {
try {
file.remove(false);
} catch (ex) {
if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
throw ex;
}
}
}
/**
* 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 = getUpdateDirFile(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;
}
/* 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;
}
/**
* Gets the specified update file or directory.
*
* @param aLogLeafName
* The leafName of the file or directory to get.
* @param aWhichDir
* Since we started having a separate patch directory and downloading
* directory, there are now files with the same name that can be in
* either directory. This argument is optional and defaults to the
* patch directory for historical reasons. But if it is specified as
* DIR_DOWNLOADING, this function will provide the version of the file
* in the downloading directory. For files that aren't in the patch
* directory or the downloading directory, this value is ignored.
* @return nsIFile for the file or directory.
*/
function getUpdateDirFile(aLeafName, aWhichDir = null) {
let file = Services.dirsvc.get(XRE_UPDATE_ROOT_DIR, Ci.nsIFile);
switch (aLeafName) {
case undefined:
return file;
case DIR_UPDATES:
case FILE_ACTIVE_UPDATE_XML:
case FILE_ACTIVE_UPDATE_XML_TMP:
case FILE_UPDATE_CONFIG_JSON:
case FILE_BACKUP_UPDATE_CONFIG_JSON:
case FILE_TEST_PROCESS_UPDATES:
case FILE_UPDATE_TEST:
case FILE_UPDATES_XML:
case FILE_UPDATES_XML_TMP:
file.append(aLeafName);
return file;
case DIR_PATCH:
case DIR_DOWNLOADING:
case FILE_BACKUP_UPDATE_LOG:
case FILE_BACKUP_UPDATE_ELEVATED_LOG:
case FILE_LAST_UPDATE_LOG:
case FILE_LAST_UPDATE_ELEVATED_LOG:
file.append(DIR_UPDATES);
file.append(aLeafName);
return file;
case FILE_UPDATE_LOG:
case FILE_UPDATE_ELEVATED_LOG:
case FILE_UPDATE_MAR:
case FILE_UPDATE_STATUS:
case FILE_UPDATE_VERSION:
case FILE_UPDATER_INI:
file.append(DIR_UPDATES);
if (aWhichDir == DIR_DOWNLOADING) {
file.append(DIR_DOWNLOADING);
} else {
file.append(DIR_PATCH);
}
file.append(aLeafName);
return file;
}
throw new Error(
"The leafName specified is not handled by this function, " +
"leafName: " +
aLeafName
);
}
/**
* 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.
* @return The nsIFile for the file in the directory where the update will be
* staged.
*/
function getStageDirFile(aRelPath) {
let file;
if (AppConstants.platform == "macosx") {
file = getUpdateDirFile(DIR_PATCH);
} else {
file = getGREBinDir();
}
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]);
}
}
}
return file;
}
/**
* Removes the update files that typically need to be removed by tests without
* removing the directories since removing the directories has caused issues
* when running tests with --verify and recursively removes the stage directory.
*
* @param aRemoveLogFiles
* When true the update log files will also be removed. This allows
* for the inspection of the log files while troubleshooting tests.
*/
function removeUpdateFiles(aRemoveLogFiles) {
let files = [
[FILE_ACTIVE_UPDATE_XML],
[FILE_UPDATES_XML],
[FILE_UPDATE_STATUS],
[FILE_UPDATE_VERSION],
[FILE_UPDATE_MAR],
[FILE_UPDATE_MAR, DIR_DOWNLOADING],
[FILE_UPDATER_INI],
];
if (aRemoveLogFiles) {
files = files.concat([
[FILE_BACKUP_UPDATE_LOG],
[FILE_LAST_UPDATE_LOG],
[FILE_UPDATE_LOG],
[FILE_BACKUP_UPDATE_ELEVATED_LOG],
[FILE_LAST_UPDATE_ELEVATED_LOG],
[FILE_UPDATE_ELEVATED_LOG],
]);
}
for (let i = 0; i < files.length; i++) {
let file = getUpdateDirFile.apply(null, files[i]);
try {
if (file.exists()) {
file.remove(false);
}
} catch (e) {
logTestInfo(
"Unable to remove file. Path: " + file.path + ", Exception: " + e
);
}
}
let stageDir = getStageDirFile();
if (stageDir.exists()) {
try {
removeDirRecursive(stageDir);
} catch (e) {
logTestInfo(
"Unable to remove directory. Path: " +
stageDir.path +
", Exception: " +
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;
}
if (!aDir.isDirectory()) {
throw new Error("Only a directory can be passed to this funtion!");
}
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.nextFile;
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);
}
/**
* 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);
}
/**
* Synchronously writes the value of the app.update.auto setting to the update
* configuration file on Windows or to a user preference on other platforms.
* When the value passed to this function is null or undefined it will remove
* the configuration file on Windows or the user preference on other platforms.
*
* @param aEnabled
* Possible values are true, false, null, and undefined. When true or
* false this value will be written for app.update.auto in the update
* configuration file on Windows or to the user preference on other
* platforms. When null or undefined the update configuration file will
* be removed on Windows or the user preference will be removed on other
* platforms.
*/
function setAppUpdateAutoSync(aEnabled) {
if (AppConstants.platform == "win") {
let file = getUpdateDirFile(FILE_UPDATE_CONFIG_JSON);
if (aEnabled === undefined || aEnabled === null) {
if (file.exists()) {
file.remove(false);
}
} else {
writeFile(
file,
'{"' + CONFIG_APP_UPDATE_AUTO + '":' + aEnabled.toString() + "}"
);
}
} else if (aEnabled === undefined || aEnabled === null) {
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_AUTO)) {
Services.prefs.clearUserPref(PREF_APP_UPDATE_AUTO);
}
} else {
Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO, aEnabled);
}
}
/**
* 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 gDebugTest 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 (gDebugTest) {
let caller = aCaller ? aCaller : Components.stack.caller;
logTestInfo(aText, caller);
}
}
/**
* Creates the continue file used to signal that update staging or the mock http
* server should continue. The delay this creates allows the tests to verify the
* user interfaces before they auto advance to other phases of an update. The
* continue file for staging will be deleted by the test updater and the
* continue file for the update check and update download requests will be
* deleted by the test http server handler implemented in app_update.sjs. The
* test returns a promise so the test can wait on the deletion of the continue
* file when necessary. If the continue file still exists at the end of a test
* it will be removed to prevent it from affecting tests that run after the test
* that created it.
*
* @param leafName
* The leafName of the file to create. This should be one of the
* folowing constants that are defined in testConstants.js:
* CONTINUE_CHECK
* CONTINUE_DOWNLOAD
* CONTINUE_STAGING
* @return Promise
* Resolves when the file is deleted or if the file is not deleted when
* the check for the file's existence times out. If the file isn't
* deleted before the check for the file's existence times out it will
* be deleted when the test ends so it doesn't affect tests that run
* after the test that created the continue file.
* @throws If the file already exists.
*/
async function continueFileHandler(leafName) {
// The total time to wait with 300 retries and the default interval of 100 is
// approximately 30 seconds.
let interval = 100;
let retries = 300;
let continueFile;
if (leafName == CONTINUE_STAGING) {
// The total time to wait with 600 retries and an interval of 200 is
// approximately 120 seconds.
interval = 200;
retries = 600;
continueFile = getGREBinDir();
if (AppConstants.platform == "macosx") {
continueFile = continueFile.parent.parent;
}
continueFile.append(leafName);
} else {
continueFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
let continuePath = REL_PATH_DATA + leafName;
let continuePathParts = continuePath.split("/");
for (let i = 0; i < continuePathParts.length; ++i) {
continueFile.append(continuePathParts[i]);
}
}
if (continueFile.exists()) {
logTestInfo(
"The continue file should not exist, path: " + continueFile.path
);
continueFile.remove(false);
}
debugDump("Creating continue file, path: " + continueFile.path);
continueFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
// If for whatever reason the continue file hasn't been removed when a test
// has finished remove it during cleanup so it doesn't affect tests that run
// after the test that created it.
registerCleanupFunction(() => {
if (continueFile.exists()) {
logTestInfo(
"Removing continue file during test cleanup, path: " + continueFile.path
);
continueFile.remove(false);
}
});
return TestUtils.waitForCondition(
() => !continueFile.exists(),
"Waiting for file to be deleted, path: " + continueFile.path,
interval,
retries
).catch(_e => {
logTestInfo(
"Continue file was not removed after checking " +
retries +
" times, path: " +
continueFile.path
);
});
}
async function waitForUpdatePing(archiveChecker, expectedProperties) {
// We cannot control when the ping will be generated/archived after we trigger
// an update, so let's make sure to have one before moving on with validation.
let updatePing;
await TestUtils.waitForCondition(
async function () {
// Check that the ping made it into the Telemetry archive.
// The test data is defined in ../data/sharedUpdateXML.js
updatePing = await archiveChecker.promiseFindPing(
"update",
expectedProperties
);
return !!updatePing;
},
"Wait for Update Ping to be generated",
500,
100
);
return updatePing;
}
/**
* It's frequently desirable to run a test many times with different parameters
* each time.
*
* @param testFn
* The function to test. It is expected to be a function that takes a
* single object of named parameters. For example:
* function test({param1, param2})
* @param parameters
* This can either be an object or an array of objects.
*
* If it is an array of objects, it will be treated as a list of
* sets of parameters. The number of times the test is executed will be
* equal to the length of the array.
* For example:
* [{param1: value1}, {param1: value2}]
*
* If it is an object, each key will be treated as a parameter with the
* corresponding value will be an array of values that parameter can
* have. The number of times the test is executed will be equal to the
* product of all the array lengths.
* For example:
* {param1: [value1, value2], {param2: [value3]}}
* @param options
* An additional object can be passed that with any of these supported
* options:
* skipFn
* This is evaluated with the test's parameters before each test. If
* it returns `true`, the test is not run with that set of
* parameters.
*/
async function parameterizedTest(testFn, parameters, { skipFn } = {}) {
logTestInfo(`parameterizedTest - Testing ${testFn.name}`);
const maybeRunTest = async params => {
const invocationDesc = `${testFn.name}(${JSON.stringify(params)})`;
if (skipFn && (await skipFn(params))) {
logTestInfo("parameterizedTest - SKIPPING " + invocationDesc);
return;
}
logTestInfo("parameterizedTest - START " + invocationDesc);
await testFn(params);
logTestInfo("parameterizedTest - COMPLETE " + invocationDesc);
};
if (Array.isArray(parameters)) {
for (const params of parameters) {
await maybeRunTest(params);
}
} else {
const recurse = async (chosenParams, remainingPossibleParams) => {
if (!remainingPossibleParams.length) {
await maybeRunTest(chosenParams);
return;
}
const [param, argValues] = remainingPossibleParams.shift();
for (const argValue of argValues) {
chosenParams[param] = argValue;
// Clone parameters when we recurse.
await recurse(Object.assign({}, chosenParams), [
...remainingPossibleParams,
]);
}
};
await recurse({}, Object.entries(parameters));
}
logTestInfo(`parameterizedTest - Finished testing ${testFn.name}`);
}
async function startBitsMarDownload(url) {
return gAUS.wrappedJSObject.makeBitsRequest({ url });
}
async function connectToBitsMarDownload(bitsId) {
return gAUS.wrappedJSObject.makeBitsRequest({ bitsId });
}
|