Mozilla/mozilla/toolkit/mozapps/update/src/nsPostUpdateWin.js
darin%meer.net 3caa1e70a1 fixes bug 307296 "MAR files do not contain updates to the uninstaller" r=bsmedberg
git-svn-id: svn://10.0.0.236/trunk@187227 18797224-902f-48f8-a5cc-f745e15eee43
2006-01-10 02:36:05 +00:00

837 lines
23 KiB
JavaScript

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Update Service.
*
* The Initial Developer of the Original Code is Google Inc.
* Portions created by the Initial Developer are Copyright (C) 2005
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Darin Fisher <darin@meer.net> (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/**
* This file contains an implementation of nsIRunnable, which may be invoked
* to perform post-update modifications to the windows registry and uninstall
* logs required to complete an update of the application. This code is very
* specific to the xpinstall wizard for windows.
*/
const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties";
const URI_UNINSTALL_PROPERTIES = "chrome://branding/content/uninstall.properties";
const KEY_APPDIR = "XCurProcD";
const KEY_COMPONENTS_DIR = "ComsD";
const KEY_PLUGINS_DIR = "APlugns";
const KEY_EXECUTABLE_FILE = "XREExeF";
// see prio.h
const PR_RDONLY = 0x01;
const PR_WRONLY = 0x02;
const PR_APPEND = 0x10;
const nsIWindowsRegKey = Components.interfaces.nsIWindowsRegKey;
//-----------------------------------------------------------------------------
/**
* Console logging support
*/
function LOG(s) {
dump("*** PostUpdateWin: " + s + "\n");
}
/**
* This function queries the XPCOM directory service.
*/
function getFile(key) {
var dirSvc =
Components.classes["@mozilla.org/file/directory_service;1"].
getService(Components.interfaces.nsIProperties);
return dirSvc.get(key, Components.interfaces.nsIFile);
}
/**
* Return the full path given a relative path and a base directory.
*/
function getFileRelativeTo(dir, relPath) {
var file = dir.clone().QueryInterface(Components.interfaces.nsILocalFile);
file.setRelativeDescriptor(dir, relPath);
return file;
}
/**
* Creates a new file object given a native file path.
* @param path
* The native file path.
* @return nsILocalFile object for the given native file path.
*/
function newFile(path) {
var file = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
file.initWithPath(path);
return file;
}
/**
* This function returns a file input stream.
*/
function openFileInputStream(file) {
var stream =
Components.classes["@mozilla.org/network/file-input-stream;1"].
createInstance(Components.interfaces.nsIFileInputStream);
stream.init(file, PR_RDONLY, 0, 0);
return stream;
}
/**
* This function returns a file output stream.
*/
function openFileOutputStream(file, flags) {
var stream =
Components.classes["@mozilla.org/network/file-output-stream;1"].
createInstance(Components.interfaces.nsIFileOutputStream);
stream.init(file, flags, 0644, 0);
return stream;
}
/**
* Gets the current value of the locale. It's possible for this preference to
* be localized, so we have to do a little extra work here. Similar code
* exists in nsHttpHandler.cpp when building the UA string.
*/
function getLocale() {
const prefName = "general.useragent.locale";
var prefs =
Components.classes["@mozilla.org/preferences-service;1"].
getService(Components.interfaces.nsIPrefBranch);
try {
return prefs.getComplexValue(prefName,
Components.interfaces.nsIPrefLocalizedString).data;
} catch (e) {}
return prefs.getCharPref(prefName);
}
//-----------------------------------------------------------------------------
const PREFIX_INSTALLING = "installing: ";
const PREFIX_CREATE_FOLDER = "create folder: ";
const PREFIX_CREATE_REGISTRY_KEY = "create registry key: ";
const PREFIX_STORE_REGISTRY_VALUE_STRING = "store registry value string: ";
function InstallLogWriter() {
}
InstallLogWriter.prototype = {
_outputStream: null, // nsIOutputStream to the install wizard log file
/**
* Write a single line to the output stream.
*/
_writeLine: function(s) {
s = s + "\n";
this._outputStream.write(s, s.length);
},
/**
* This function creates an empty install wizard log file and returns
* a reference to the resulting nsIFile.
*/
_getInstallLogFile: function() {
function makeLeafName(index) {
return "install_wizard" + index + ".log"
}
var file = getFile(KEY_APPDIR);
file.append("uninstall");
if (!file.exists())
return null;
file.append("x");
var n = 0;
do {
file.leafName = makeLeafName(++n);
} while (file.exists());
if (n == 1)
return null; // What, no install wizard log file?
file.leafName = makeLeafName(n - 1);
return file;
},
/**
* Return the update.log file. Use last-update.log file in case the
* updates/0 directory has already been cleaned out (see bug 311302).
*/
_getUpdateLogFile: function() {
var file = getFile(KEY_APPDIR);
file.append("updates");
file.append("0");
file.append("update.log");
if (file.exists())
return file;
file = getFile(KEY_APPDIR);
file.append("updates");
file.append("last-update.log");
if (file.exists())
return file;
return null;
},
/**
* Read update.log to extract information about files that were
* newly added for this update.
*/
_readUpdateLog: function(logFile, entries) {
var appDir = getFile(KEY_APPDIR);
var stream;
try {
stream = openFileInputStream(logFile).
QueryInterface(Components.interfaces.nsILineInputStream);
var dirs = {};
var line = {};
while (stream.readLine(line)) {
var data = line.value.split(" ");
if (data[0] == "EXECUTE" && data[1] == "ADD") {
var file = getFileRelativeTo(appDir, data[2]);
// remember all parent directories
var parent = file.parent;
while (!parent.equals(appDir)) {
dirs[parent.path] = true;
parent = parent.parent;
}
// remember the file
entries.files.push(file.path);
}
}
for (var d in dirs)
entries.dirs.push(d);
// Sort the directories so that subdirectories are deleted first.
entries.dirs.sort();
} finally {
if (stream)
stream.close();
}
},
/**
* This function initializes the log writer and is responsible for
* translating 'update.log' to the install wizard format.
*/
begin: function() {
var installLog = this._getInstallLogFile();
var updateLog = this._getUpdateLogFile();
if (!installLog || !updateLog)
return;
var entries = { dirs: [], files: [] };
this._readUpdateLog(updateLog, entries);
this._outputStream =
openFileOutputStream(installLog, PR_WRONLY | PR_APPEND);
this._writeLine("\nUPDATE [" + new Date().toUTCString() + "]");
var i;
// The log file is processed in reverse order, so list directories before
// files to ensure that the directories will be deleted.
for (i = 0; i < entries.dirs.length; ++i)
this._writeLine(PREFIX_CREATE_FOLDER + entries.dirs[i]);
for (i = 0; i < entries.files.length; ++i)
this._writeLine(PREFIX_INSTALLING + entries.files[i]);
},
/**
* This function records the creation of a registry key.
*/
registryKeyCreated: function(keyPath) {
if (!this._outputStream)
return;
this._writeLine(PREFIX_CREATE_REGISTRY_KEY + keyPath + " []");
},
/**
* This function records the creation of a registry key value.
*/
registryKeyValueSet: function(keyPath, valueName) {
if (!this._outputStream)
return;
this._writeLine(
PREFIX_STORE_REGISTRY_VALUE_STRING + keyPath + " [" + valueName + "]");
},
end: function() {
if (!this._outputStream)
return;
this._outputStream.close();
this._outputStream = null;
}
};
var installLogWriter;
//-----------------------------------------------------------------------------
/**
* A thin wrapper around nsIWindowsRegKey that keeps track of its path
* and notifies the installLogWriter when modifications are made.
*/
function RegKey() {
// Internally, we may pass parameters to this constructor.
if (arguments.length == 3) {
this._key = arguments[0];
this._root = arguments[1];
this._path = arguments[2];
} else {
this._key =
Components.classes["@mozilla.org/windows-registry-key;1"].
createInstance(nsIWindowsRegKey);
}
}
RegKey.prototype = {
_key: null,
_root: null,
_path: null,
ACCESS_READ: nsIWindowsRegKey.ACCESS_READ,
ACCESS_WRITE: nsIWindowsRegKey.ACCESS_WRITE,
ACCESS_ALL: nsIWindowsRegKey.ACCESS_ALL,
ROOT_KEY_CURRENT_USER: nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
ROOT_KEY_LOCAL_MACHINE: nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
close: function() {
this._key.close();
this._root = null;
this._path = null;
},
open: function(rootKey, path, mode) {
this._key.open(rootKey, path, mode);
this._root = rootKey;
this._path = path;
},
openChild: function(path, mode) {
var child = this._key.openChild(path, mode);
return new RegKey(child, this._root, this._path + "\\" + path);
},
createChild: function(path, mode) {
var child = this._key.createChild(path, mode);
var key = new RegKey(child, this._root, this._path + "\\" + path);
if (installLogWriter)
installLogWriter.registryKeyCreated(key.toString());
return key;
},
readStringValue: function(name) {
return this._key.readStringValue(name);
},
writeStringValue: function(name, value) {
this._key.writeStringValue(name, value);
if (installLogWriter)
installLogWriter.registryKeyValueSet(this.toString(), name);
},
writeIntValue: function(name, value) {
this._key.writeIntValue(name, value);
if (installLogWriter)
installLogWriter.registryKeyValueSet(this.toString(), name);
},
hasValue: function(name) {
return this._key.hasValue(name);
},
hasChild: function(name) {
return this._key.hasChild(name);
},
get childCount() {
return this._key.childCount;
},
getChildName: function(index) {
return this._key.getChildName(index);
},
removeChild: function(name) {
this._key.removeChild(name);
},
toString: function() {
var root;
switch (this._root) {
case this.ROOT_KEY_LOCAL_MACHINE:
root = "HKEY_LOCAL_MACHINE";
break;
case this.ROOT_KEY_CURRENT_USER:
root = "HKEY_CURRENT_USER";
break;
default:
LOG("unknown root key");
return "";
}
return root + "\\" + this._path;
}
};
//-----------------------------------------------------------------------------
/*
RegKey.prototype = {
// The name of the registry key
name: "";
// An array of strings, where each even-indexed string names a value,
// and the subsequent string provides data for the value.
values: [];
// An array of RegKey objects.
children: [];
}
*/
/**
* This function creates a heirarchy of registry keys. If any of the
* keys or values already exist, then they will be updated instead.
* @param rootKey
* The root registry key from which to create the new registry
* keys.
* @param data
* A JS object with properties: "name", "values", and "children"
* as defined above. All children of this key will be created.
*/
function createRegistryKeys(rootKey, data) {
var key;
try {
key = rootKey.createChild(data.name, rootKey.ACCESS_WRITE);
var i;
if ("values" in data) {
for (i = 0; i < data.values.length; i += 2)
key.writeStringValue(data.values[i], data.values[i + 1]);
}
if ("children" in data) {
for (i = 0; i < data.children.length; ++i)
createRegistryKeys(key, data.children[i]);
}
key.close();
} catch (e) {
LOG(e);
if (key)
key.close();
}
}
/**
* This function deletes the specified registry key and optionally all of its
* children.
* @param rootKey
* The parent nsIwindowRegKey of the key to delete.
* @param name
* The name of the key to delete.
* @param recurse
* Pass true to also delete all children of the named key. Take care!
*/
function deleteRegistryKey(rootKey, name, recurse) {
if (!rootKey.hasChild(name)) {
LOG("deleteRegistryKey: rootKey does not have child: \"" + name + "\"");
return;
}
if (recurse) {
var key = rootKey.openChild(name, rootKey.ACCESS_ALL);
try {
for (var i = key.childCount - 1; i >= 0; --i)
deleteRegistryKey(key, key.getChildName(i), true);
} finally {
key.close();
}
}
rootKey.removeChild(name);
}
/**
* This method walks the registry looking for the registry keys of
* the previous version of the application.
*/
function locateOldInstall(key, ourInstallDir) {
var result, childKey, productKey, mainKey;
try {
for (var i = 0; i < key.childCount; ++i) {
var childName = key.getChildName(i);
childKey = key.openChild(childName, key.ACCESS_READ);
if (childKey.hasValue("CurrentVersion")) {
for (var j = 0; j < childKey.childCount; ++j) {
var productVer = childKey.getChildName(j);
productKey = childKey.openChild(productVer, key.ACCESS_READ);
if (productKey.hasChild("Main")) {
mainKey = productKey.openChild("Main", key.ACCESS_READ);
var installDir = mainKey.readStringValue("Install Directory");
var menuPath = mainKey.readStringValue("Program Folder Path");
mainKey.close();
if (newFile(installDir).equals(ourInstallDir)) {
result = new Object();
result.fullName = childName;
result.versionWithLocale = productVer;
result.version = productVer.split(" ")[0];
result.menuPath = menuPath;
}
}
productKey.close();
if (result)
break;
}
}
childKey.close();
if (result)
break;
}
} catch (e) {
result = null;
if (childKey)
childKey.close();
if (productKey)
productKey.close();
if (mainKey)
mainKey.close();
}
return result;
}
/**
* Delete registry keys left-over from the previous version of the app
* installed at our location.
*/
function deleteOldRegKeys(key, info) {
deleteRegistryKey(key, info.fullName + " " + info.version, true);
var productKey = key.openChild(info.fullName, key.ACCESS_ALL);
var productCount;
try {
deleteRegistryKey(productKey, info.versionWithLocale, true);
productCount = productKey.childCount;
} finally {
productKey.close();
}
if (productCount == 0)
key.removeChild(info.fullName);
}
/**
* The installer sets various registry keys and values that may need to be
* updated.
*
* This operation is a bit tricky since we do not know the previous value of
* brandFullName. As a result, we must walk the registry looking for an
* existing key that references the same install directory. We assume that
* the value of vendorShortName does not change across updates.
*/
function updateRegistry(rootKey) {
LOG("updateRegistry");
var ourInstallDir = getFile(KEY_APPDIR);
var app =
Components.classes["@mozilla.org/xre/app-info;1"].
getService(Components.interfaces.nsIXULAppInfo).
QueryInterface(Components.interfaces.nsIXULRuntime);
var sbs =
Components.classes["@mozilla.org/intl/stringbundle;1"].
getService(Components.interfaces.nsIStringBundleService);
var brandBundle = sbs.createBundle(URI_BRAND_PROPERTIES);
var brandFullName = brandBundle.GetStringFromName("brandFullName");
var vendorShortName;
try {
// The Thunderbird vendorShortName is "Mozilla Thunderbird", but we
// just want "Thunderbird", so allow it to be overridden in prefs.
var prefs =
Components.classes["@mozilla.org/preferences-service;1"].
getService(Components.interfaces.nsIPrefBranch);
vendorShortName = prefs.getCharPref("app.update.vendorName.override");
}
catch (e) {
vendorShortName = brandBundle.GetStringFromName("vendorShortName");
}
var key = new RegKey();
var oldInstall;
try {
key.open(rootKey, "SOFTWARE\\" + vendorShortName, key.ACCESS_READ);
oldInstall = locateOldInstall(key, ourInstallDir);
} finally {
key.close();
}
if (!oldInstall) {
LOG("no existing registry keys found");
return;
}
// Maybe nothing needs to be changed...
if (oldInstall.fullName == brandFullName &&
oldInstall.version == app.version) {
LOG("registry is up-to-date");
return;
}
// Delete the old keys:
try {
key.open(rootKey, "SOFTWARE\\" + vendorShortName, key.ACCESS_READ);
deleteOldRegKeys(key, oldInstall);
} finally {
key.close();
}
// Create the new keys:
var versionWithLocale = app.version + " (" + getLocale() + ")";
var installPath = ourInstallDir.path + "\\";
var pathToExe = getFile(KEY_EXECUTABLE_FILE).path;
var Key_bin = {
name: "bin",
values: [
"PathToExe", pathToExe
]
};
var Key_extensions = {
name: "Extensions",
values: [
"Components", getFile(KEY_COMPONENTS_DIR).path + "\\",
"Plugins", getFile(KEY_PLUGINS_DIR).path + "\\"
]
};
var Key_nameWithVersion = {
name: brandFullName + " " + app.version,
values: [
"GeckoVer", app.platformVersion
],
children: [
Key_bin,
Key_extensions
]
};
var Key_main = {
name: "Main",
values: [
"Install Directory", installPath,
"PathToExe", pathToExe,
"Program Folder Path", oldInstall.menuPath
]
};
var Key_uninstall = {
name: "Uninstall",
values: [
"Description", brandFullName + " (" + app.version + ")",
"Uninstall Log Folder", installPath + "uninstall"
]
};
var Key_versionWithLocale = {
name: versionWithLocale,
children: [
Key_main,
Key_uninstall
]
};
var Key_name = {
name: brandFullName,
values: [
"CurrentVersion", versionWithLocale
],
children: [
Key_versionWithLocale
]
};
var Key_brand = {
name: vendorShortName,
children: [
Key_name,
Key_nameWithVersion
]
};
try {
key.open(rootKey, "SOFTWARE", key.ACCESS_READ);
createRegistryKeys(key, Key_brand);
} finally {
key.close();
}
if (rootKey != RegKey.prototype.ROOT_KEY_LOCAL_MACHINE)
return;
// Now, do the same thing for the Add/Remove Programs control panel:
const uninstallRoot =
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
try {
key.open(rootKey, uninstallRoot, key.ACCESS_READ);
var oldName = oldInstall.fullName + " (" + oldInstall.version + ")";
deleteRegistryKey(key, oldName, false);
} finally {
key.close();
}
var uninstallBundle = sbs.createBundle(URI_UNINSTALL_PROPERTIES);
var nameWithVersion = brandFullName + " (" + app.version + ")";
var uninstaller = getFile(KEY_APPDIR);
uninstaller.append("uninstall");
uninstaller.append("uninstall.exe");
var uninstallString =
uninstaller.path + " /ua \"" + versionWithLocale + "\"";
Key_uninstall = {
name: nameWithVersion,
values: [
"Comment", brandFullName,
"DisplayIcon", pathToExe + ",0", // XXX don't hardcode me!
"DisplayName", nameWithVersion,
"DisplayVersion", versionWithLocale,
"InstallLocation", ourInstallDir.path, // no trailing slash
"Publisher", vendorShortName,
"UninstallString", uninstallString,
"URLInfoAbout", uninstallBundle.GetStringFromName("URLInfoAbout"),
"URLUpdateInfo", uninstallBundle.GetStringFromName("URLUpdateInfo")
]
};
var child;
try {
key.open(rootKey, uninstallRoot, key.ACCESS_READ);
createRegistryKeys(key, Key_uninstall);
// Create additional DWORD keys for NoModify and NoRepair:
child = key.openChild(nameWithVersion, key.ACCESS_WRITE);
child.writeIntValue("NoModify", 1);
child.writeIntValue("NoRepair", 1);
} finally {
if (child)
child.close();
key.close();
}
}
//-----------------------------------------------------------------------------
function nsPostUpdateWin() {
}
nsPostUpdateWin.prototype = {
QueryInterface: function(iid) {
if (iid.equals(Components.interfaces.nsIRunnable) ||
iid.equals(Components.interfaces.nsISupports))
return this;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
run: function() {
try {
// We use a global object here for the install log writer, so that the
// registry updating code can easily inform it of registry keys that it
// may create.
installLogWriter = new InstallLogWriter();
try {
installLogWriter.begin();
updateRegistry(RegKey.prototype.ROOT_KEY_CURRENT_USER);
updateRegistry(RegKey.prototype.ROOT_KEY_LOCAL_MACHINE);
} finally {
installLogWriter.end();
installLogWriter = null;
}
} catch (e) {
LOG(e);
}
}
};
//-----------------------------------------------------------------------------
var gModule = {
registerSelf: function(compMgr, fileSpec, location, type) {
compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
for (var key in this._objects) {
var obj = this._objects[key];
compMgr.registerFactoryLocation(obj.CID, obj.className, obj.contractID,
fileSpec, location, type);
}
},
getClassObject: function(compMgr, cid, iid) {
if (!iid.equals(Components.interfaces.nsIFactory))
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
for (var key in this._objects) {
if (cid.equals(this._objects[key].CID))
return this._objects[key].factory;
}
throw Components.results.NS_ERROR_NO_INTERFACE;
},
_makeFactory: #1= function(ctor) {
function ci(outer, iid) {
if (outer != null)
throw Components.results.NS_ERROR_NO_AGGREGATION;
return (new ctor()).QueryInterface(iid);
}
return { createInstance: ci };
},
_objects: {
manager: { CID : Components.ID("{d15b970b-5472-40df-97e8-eb03a04baa82}"),
contractID : "@mozilla.org/updates/post-update;1",
className : "nsPostUpdateWin",
factory : #1#(nsPostUpdateWin)
},
},
canUnload: function(compMgr) {
return true;
}
};
function NSGetModule(compMgr, fileSpec) {
return gModule;
}