Wednesday, August 19, 2009

Scripted hiding of Windows Updates under Vista

Similar to my last post, here is another UI issue with Windows Vista. Fortunately, this time I have a solution to offer.

Starting with Windows Vista, the "Windows Update" functionality is provided through Control Panel rather than Internet Explorer. In both versions, there is the ability to hide updates. While hidden updates can easily be restored, this feature allows for ignoring unnecessary updates so that they don't continually count towards the number of available updates that are displayed. For me, this includes the 34 "Windows Vista Ultimate Language Packs" that are currently available. Unfortunately, multiple-selection is not enabled in the "View available updates" dialog. There are checkboxes, including a checkbox on the header that can be used to select/unselect all shown updates, but the checkbox selections are used for the "Install" button only. The other options available from the context menu - "View details", "Copy details", and "Hide update" - can currently be applied only one-at-a-time. This means that hiding just the 34 language packs would require no fewer than 68 clicks!

Originally, I assumed that these hidden updates and other preferences would be stored in the Windows registry, or possibly in a file on the file system. They are in a file, but a database-type file that isn't directly editable: %SystemRoot%\SoftwareDistribution\DataStore\DataStore.edb. Fortunately, there is a comprehensive Windows API for viewing and editing this information, and it is even easily available to scripting through the Windows Scripting Host and languages such as JScript. Microsoft's reference is located on MSDN: Windows Update Agent API.

Here is my resulting script that automatically hides all the "Windows Vista Ultimate Language Packs":

var updateSession = WScript.CreateObject("Microsoft.Update.Session");
var updateSearcher = updateSession.CreateUpdateSearcher();
updateSearcher.Online = false;

var searchResult = updateSearcher.Search("CategoryIDs Contains 'a901c1bd-989c-45c6-8da0-8dde8dbb69e0' And IsInstalled=0");

for(var i=0; i<searchResult.Updates.Count; i++){
  var update = searchResult.Updates.Item(i);
  WScript.echo("Hiding update: " + update.Title);
  update.IsHidden = true;
}

If you're not familiar with WSH, this can be simply executed as saving it as a *.js file, then double-clicking. A better option is to execute the file from a command-line with cscript. This will cause the output messages to be written to the standard output, instead of popping up a message box that must be acknowledged for each message. Also, since this script is making administrative changes to the system, it must be executed as an administrator.

"a901c1bd-989c-45c6-8da0-8dde8dbb69e0" is the ICategory.CategoryID for "Windows Vista Ultimate Language Packs". (This ICategory happens to have a .Type of "Product".) A similar script can easily be used to perform operations on other sets of updates by simply modifying the search query.

For the above example, the changes can be reverted by updating the script to executed update.IsHidden = false; (instead of true), then re-executing the script. Alternatively, here the Windows Vista GUI works a little better: By clicking on "Restore hidden updates" from the side panel in Windows Update, the "Restore" button operates on the checkbox selection - allowing all hidden updates to quickly be restored with 2 clicks if desired.

Finally, here is an extended example that doesn't change anything, but displays some of the many details that are available through this API. First, it displays all the updates grouped and nested by category. Note that some updates belong to more than one category. Finally, it displays all available updates in a "flat" view, without using categories.

var updateSession = WScript.CreateObject("Microsoft.Update.Session");
var updateSearcher = updateSession.CreateUpdateSearcher();
updateSearcher.Online = false;

var searchResult = updateSearcher.Search("IsInstalled=1 or IsInstalled=0");

var describeCategory = function(cat, depth){
  var pad = new Array(depth + 1).join("  ");
  WScript.echo(pad + depth + ": " + cat + ", " + cat.CategoryID + ", " + cat.Name + ", " + cat.Type);

  for(var i=0; i<cat.Children.Count; i++){
    var child = cat.Children.Item(i);
    describeCategory(child, depth + 1);
  }
  
  for(var i=0; i<cat.Updates.Count; i++){
    var update = cat.Updates.Item(i);
    WScript.echo(pad + "  " + describeUpdate(update, pad + "  "));
  }
};

var describeUpdate = function(update, pad){
  var u = update;
  var np = "\n" + (pad || "") + "  ";
  return u.Title
    + np + "Type: " + u.Type
    //+ np + "Description: " + u.Description
    + np + "IsInstalled: " + u.IsInstalled
    + np + "IsDownloaded: " + u.IsDownloaded
    + np + "IsHidden: " + u.IsHidden
    + np + "AutoSelectOnWebSites: " + u.AutoSelectOnWebSites;
};

for(var i=0; i<searchResult.RootCategories.Count; i++){
  var category = searchResult.RootCategories.Item(i);
  describeCategory(category, 1);
}

WScript.echo("\n");

for(var i=0; i<searchResult.Updates.Count; i++){
  var update = searchResult.Updates.Item(i);
  WScript.echo(describeUpdate(update));
}

According to the IUpdateSearcher.Search documentation, the default search criteria is "IsInstalled = 0 and IsHidden = 0". Unfortunately, there doesn't seem to be a simple option to short-circuit the evaluator to just return all available updates, e.g. "" or "1=1". So far now, "IsInstalled=1 or IsInstalled=0" results in all updates being displayed. The only other note concerning the above example is that the "description" line is commented out in the describeUpdate function only because it can be rather verbose, and make the overall output difficult to read. Feel free to uncomment it to view the details, as well as adding additional lines for all the other properties available from IUpdate.

8 comments:

Daniel said...

Cool script. You were high up on the Google results. Always people with similar annoyances. Thanks a lot.

Anonymous said...

Hi Mark,

Only a question about that.

If I execute the script when WU is activated (i.e. notifying or downloading automatically) then it hides perfectly the updates I don't want (for example languages).

But, If WU is deactivated, the script is executed but does not change anything because if you activate WU and search for updates then the languages are there.

Is it possible to hide the updates independently of the WU status?

Thanks

Anonymous said...

Hello Mark!

Can You more digger for me and help Me find
ICategory.CategoryID for "Windows 7 Ultimate Language Packs"?
I need use your tricks to Windows7.
Maybe you got file.reg solution above srcipt hiding 34 languages?
thnx

Anonymous said...

I Found
6d76a2a5-81fe-4829-b268-6eb307e40ef3.
Can you insert for loop autoenter 34 times?

Mark A. Ziesemer said...

Anonymous from May 14-15: I'm not exactly sure what you're asking, but there's already a loop in the script, which will hide each update returned from the search result. As long as all of the language packs are in a category in Windows 7 like they were in Windows Vista, you just need to use the ICategory.CategoryID as it looks like you were already thinking. I'm not sure what the GUID you next posted is, but if it's a CategoryID, you should be all set. If it's an individual language pack, then you just need to find the parent "language pack" CategoryID. You should be able to use the 2nd script on the post to help find the desired IDs.

Anonymous said...

Mark I tried to find the CategoryID for Windows 7 Updates using your second script and I do not see it listed in the output. What am I doing wrong?

Anonymous said...

Hello Mark!

Can You help Me find
ICategory.CategoryID for "Windows Malicious Software Removal Tool x64". Thanks for your help this is awesome.

NDog said...

Hi Mark!

Can you help me to hide the Microsoft Security Essentials update?

The code (does not work) I am using is follows

dim strTitle


Set updateSession = CreateObject("Microsoft.Update.Session")
Set updateSearcher = updateSession.CreateupdateSearcher()

updateSearcher.ServerSelection = ssWindowsUpdate
Set searchResult = updateSearcher.Search("IsInstalled=0 and Type='Software'")

For I = 0 To searchResult.Updates.Count-1
Set update = searchResult.Updates.Item(I)
strTitle = update.Title
'WScript.Echo strTitle
if InStr(intSearchStartChar, strTitle, "Microsoft Security Essentials - KB2267621", vbTextCompare) <> "0" then
update.IsHidden = True
end if
Next

WScript.Quit