Created
January 5, 2017 09:09
-
-
Save native-api/77b7399955195a8ef489e2d3120e5543 to your computer and use it in GitHub Desktop.
utility functions to work with Windows Installer registry database
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf-8 | |
""" | |
Some utility functions to work with Windows Installer database in registry. | |
(Worked it out while rebuilding deleted %windir%\Installer) | |
Terminology used in fn names: | |
* patchid,productid - corresponding GUIDs as they are used in key names, e.g. CC458296FE7970347B78C876789B0194 | |
* msx/fname - original package file name as it is in HKCR\Installer\SourceList:PackageName | |
* local - full path to cached file in %windir%\installer | |
""" | |
import os,os.path | |
import shutil | |
import re | |
import winsys.registry as reg | |
ksources=reg.registry(r"HKEY_CLASSES_ROOT\Installer\Patches") | |
klocals=reg.registry(r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Patches") | |
kproducts=reg.registry(r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products") | |
kallsources=reg.registry(r'HKEY_CLASSES_ROOT\Installer') | |
def patchid2productid(patchid): | |
"""product a patch belongs to""" | |
for key,keys,values in kproducts.walk(): | |
if key.name!='Patches': continue | |
if patchid in list(k.name for k in keys): return key.parent().name | |
return None | |
def patchid_is_orphaned(id_): | |
"""a patch is orphaned if it doesn't belong to any installed product""" | |
return patchid2productid(id_) is None | |
def patchid2local(id_): | |
return klocals[id_].LocalPackage | |
def msx2patchid(msx): | |
"""find patches that match msx, case-insensitively. | |
If only one is found, it doesn't care if it's orphaned (for the task at hand, it was easier that way)""" | |
msx=msx.lower() | |
result=[] | |
for key,keys,values in ksources.walk(): | |
if os.path.basename(key.moniker)=='SourceList' and hasattr(key,'PackageName') and key.PackageName.lower()==msx: | |
id_ = key.parent().name | |
result.append(id_) | |
if not result: raise KeyError(msx) | |
if len(result)==1: return result | |
no_result=[id_ for id_ in result if not patchid_is_orphaned(id_)] | |
if len(no_result)>1: | |
productids=set(patchid2productid(id_) for id_ in no_result) | |
if len(productids)>1: | |
raise ValueError("`%s' matches multiple non-orphaned patches %r for different products %r"%(msx,no_result,productids)) | |
return no_result | |
def productid2local(id_): | |
return kproducts[id_]['InstallProperties'].LocalPackage | |
def local2patchid(local): | |
for key in klocals: | |
if key.LocalPackage==local: return key.name | |
raise KeyError(local) | |
def missingpatches(productid): | |
"""patches for a product with missing locals""" | |
allpatches=[key.name for key in kproducts+productid+'Patches'] | |
result=[patchid for patchid in allpatches if not os.path.exists(patchid2local(patchid))] | |
return result | |
def patchid2fname(id_): | |
return (kallsources+'Patches'+id_+'SourceList').PackageName | |
def fname2kburl(fname): | |
"""MS KB URL that an fname refers to""" | |
m=re.search('-kb(\d{6,})\.ms[a-z]$',fname,re.I) | |
if not m: raise ValueError(fname) | |
return 'http://support.microsoft.com/en-us/kb/%s'%(m.group(1),) | |
def searchbyinstdir(dirs): | |
"""ids by original installation source path component(s)""" | |
if isinstance(dirs,basestring):dirs=(dirs,) | |
result=[] | |
for key,keys,values in kallsources.walk(): | |
if key.name!='Net':continue | |
if not any(n for n,v in values if all(dir_ in v.split(os.path.sep) for dir_ in dirs)):continue | |
fname=key.parent().PackageName | |
id_=key.parent().parent().name | |
type_=key.parent().parent().parent().name | |
result.append((type_,id_,fname)) | |
return result | |
def sample1(): | |
productid2local('00002109810091400000000000F01FEC') | |
patchid2fname('624B02B22C0AE503691A62B10223C5FE') | |
local2patchid(r'D:\WINDOWS\Installer\21ecedb7.msp') | |
os.path.exists(r'D:\WINDOWS\Installer\21ecedb7.msp') | |
patchid_is_orphaned('9C0483E293578493A8D1B33F21DBEA92') | |
patchid2productid('63A0A088B442A7C3D8B6656E49EC8738') | |
missingpatches('0DC1503A46F231838AD88BCDDC8E8F7C') | |
def sample2(): | |
"""copy all .msp's from unpacked .NET 3.5 distribution to locals.""" | |
""".NET .msi's don't have files inside them so they can be copied as-is, | |
no need to prepare a stripped one with msiexec /a""" | |
#get_ipython().magic(u'cd C:\\Documents and Settings\\Administrator\\My Documents\\Desktop\\dotnetfx35langpack_x64ru\\netfx20lp') | |
for type_,id_,fname in searchbyinstdir(("dotnetfx35","x64")): | |
if type_=='Patches': local=patchid2local(id_) | |
elif type_=='Products': local=productid2local(id_) | |
else: raise KeyError(type_) | |
print fname,local | |
shutil.copyfile(fname,local) | |
def sample3(): | |
"""open KB web pages for all the locally missing patches for a product""" | |
import win32api | |
for url in [fname2kburl(patchid2fname(id_)) for id_ in missingpatches('26DDC2EC4210AC63483DF9D4FCC5B59D')]: | |
win32api.ShellExecute(None,None,url,None,None,0) | |
def sample4(): | |
"""copy .msp's from updates unpacked to current dir to locals; | |
if a local is already present, call `ls' to compare them""" | |
#get_ipython().magic(u'cd C:\\Documents and Settings\\Administrator\\My Documents\\Desktop\\U') | |
import subprocess | |
for fname in (fname for fname in os.walk('.').next()[2] if re.match(r'.*\.msp$',fname)): | |
ids=msx2patchid(fname) | |
locals_=[patchid2local(id_) for id_ in ids] | |
print fname,locals_ | |
for local in locals_: | |
exists=os.path.exists(local) | |
print exists | |
if exists: subprocess.call(('ls','-l',fname,local)) | |
if not exists: shutil.copyfile(fname,local) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment