diff options
Diffstat (limited to 'bps/host/windows.py')
| -rw-r--r-- | bps/host/windows.py | 928 |
1 files changed, 928 insertions, 0 deletions
diff --git a/bps/host/windows.py b/bps/host/windows.py new file mode 100644 index 0000000..8addb49 --- /dev/null +++ b/bps/host/windows.py @@ -0,0 +1,928 @@ +"""windows backend""" +#========================================================= +#imports +#========================================================= +from __future__ import with_statement +#core +from logging import getLogger +import os.path +import subprocess +import sys +from warnings import warn +try: + import _winreg as winreg +except ImportError: + winreg = None #doing this so docs can be built from linux +from contextlib import contextmanager +import threading +#pkg +from bps.types import BaseClass, Undef +from bps.refs import SoftValueDict +from bps.fs import filepath, posix_to_local +from bps.host.base import BaseBackend, UserProfile +#local +log = getLogger(__name__) + +if hasattr(winreg, "ExpandEnvironmentStrings"): #added in py26 + _ExpandEnvironmentStrings = winreg.ExpandEnvironmentStrings +else: + _ExpandEnvironmentStrings = os.path.expandvars + +#========================================================= +#nt +#========================================================= +#TODO: should implement a CygWin backend, and have NT_Backend check for it +#TODO: kde4 can run under windows, should check for it. + +class WindowsBackend(BaseBackend): + + #========================================================= + #instance attrs + #========================================================= + profile = None #profile name (nt, 95) determining resource path locations + + #resource discovery + env_user = None #environ_user info + + wine = False #are we running under wine? + + #========================================================= + #init + #========================================================= + def __init__(self, **kwds): + self.__super.__init__(**kwds) + + #check for wine +## #this might not be a reliable test... +## try: #keeping this protected till regpath gets more testing +## if regpath("/local_machine/software/wine/drives").exists: +## self.wine = True +## log.info("WINE detected") +## except: +## log.error("error in regpath:", exc_info=True) + #TODO: if wine _is_ present, would like to choose user's real home dir / config dir etc + + # need to check if cygwin sets same key, + # and it might be cleared anyways +## pwd = os.environ.get("PWD") +## if pwd and pwd.startswith("/"): +## #best guess, we're probably running under wine +## pass + + + #determine profile + major, minor, build, platform, text = version = sys.getwindowsversion() + if platform == 2: #nt/2k/xp + self.profile = "nt" + elif platform == 1: #windows 95/98/ME + self.profile = "95" + else: + #FIXME: don't have access to 3.1(platform=0) or CE(platform=3) for testing + raise EnvironmentError, "unsupported windows version: %r" % (version,) + + #load custom exe_exts... + #first, load PATHEXT if present... + #';' separated list of extensions (eg: ".EXE;.BAT") which will be searched for by cmd.exe + exts = os.environ.get("PATHEXT") + if exts: + self.exe_exts = tuple(exts.split(os.path.pathsep)) + + #========================================================= + #process management + #========================================================= + def terminate_pid(self, pid, retry, kill, timeout): + #FIXME: would like to try _something_ for _termiante_pid() + #SEE: http://www.ddj.com/windows/184416547 for ideas about how + warn("stub: terminate_pid() not implemented for win32, using kill_pid()", RuntimeWarning) + return self.kill_pid(pid, retry, timeout) + + def _kill_pid(self, pid): + #FIXME: need to pass sig in + #os.kill isn't available for win32, so use win32api + import win32api + handle = win32api.OpenProcess(1, 0, pid) + #TODO: log an error/warning if terminate fails, maybe even figure out why + #XXX: make sure this returns 'True' if signal sent, and 'False' if process not found + + if win32api.TerminateProcess(handle, 0) == 0: + #XXX: MS sez check GetLastError() to find out why. + return False #could check, but assuming the signal failed to be sent cause proc was gone. + else: #assume it was sent + return True + + def has_pid(self, pid): + #found at http://mail.python.org/pipermail/spambayes-checkins/2003-December/002383.html + import win32process + import win32con + try: + rc = win32process.GetExitCodeProcess(pid) + return rc == win32con.STILL_ACTIVE + except win32process.error: + return False + + #========================================================= + #shell integration + #========================================================= + exe_exts = (".com", ".exe", ".bat",) + + #TODO: find_exe() - saw something that said windows will only + # consider exact match if extension is specified (eg "app.exe", "app.bat"), + # but will look at all exe_exts if no extension is present. + # need to verify this, as well as what "foo.bar" would do + + #========================================================= + #desktop interaction + #========================================================= + def detect_desktop(self): + #XXX: use orig when kde detection is integrated + return "windows" + + def desktop_open(self, path, action, mimetype): + #XXX: if desktop not windows, need to use alt handler + #TODO: have code hunt down available actions in registry, for better choosing + waction = { + "open": "open", #duh + "view": "open", #view never really registered + "edit": "open", #edit never really registered + #print - rare + #exec - not seen + "browse": "explore", + }.get(action, action) + if waction == "exec": + log.warning("'exec' action has not been tested much on win32") + try: + os.startfile(path, waction) + except WindowsError, err: + if err.args[0] == 1155: # No application is associated with the specified file for this operation + if action is not None: + #fallback to default action + log.warning("no handler for action: action=%r path=%r", action, path) + os.startfile(path) + #FIXME: what if startfile fails again? + # should we raise special error, or just return false? + # _can_ it ever raise an error in this context? + return + raise + + #========================================================= + #resource discovery + #========================================================= + def load_resources(self): + self.__super.load_resources() + if self.profile == "nt": + self.load_nt_resources() + else: + assert self.profile == "95" + self.load_95_resources() + + def load_95_resources(self): + #FIXME: don't have this OS available for testing. + #where is everyting, with or w/o user profiles? + #provided a cheap guess here + WinDir = filepath(os.environ.get("WINDIR",None)) + if not WinDir or WinDir.ismissing: + raise EnvironmentError, "can't find windows install" + profile = self.env + profile.state_dir = WinDir / "Application Data" + if profile.state_dir.ismissing: + profile.state_dir.makedirs() + profile.home_dir = WinDir #FIXME: could do better than this, even under win95 + + def load_nt_resources(self): + profile = self.env + get = os.environ.get + profile.login = get("USERNAME") #XXX: could guess defaults based on login + + #find home directory... store in HomePath + while True: + #check for user profile + HomePath = filepath(get("USERPROFILE")) + if HomePath and HomePath.exists: + break + #check for HomeDrive / HomePath + drive = get("HOMEDRIVE") + path = get("HOMEPATH") + if drive and path: + HomePath = filepath(drive, path) + if HomePath.exists: + break + #give up + raise EnvironmentError, "can't find user's home directory" + profile.home_dir = HomePath + + #check for appdata... + AppDataPath = filepath(get("APPDATA")) + if AppDataPath and AppDataPath.exists: + profile.state_dir = AppDataPath + else: + #TODO: like to try something else before we fall back to pre-nt behavior... + profile.state_dir = HomePath / "Application Data" + if profile.state_dir.ismissing: + profile.state_dir.makedirs() + + self._fill_user_info(profile, is_env=True) + + #----------------------------------------------- + #user related info + #----------------------------------------------- + def user_by_login(self, login, missing): + "return UserInfo for user w/ matching login" + raise NotImplementedError, "implement me!" + + def user_by_uid(self, uid, missing): + "return UserInfo for user w/ matching uid" + if missing == "error": + raise KeyError, "UIDs are not assigned by windows: %r" % uid + else: + warn("UIDs are not assigned by windows, so no match is possible: %r" % uid) + return None + + def _fill_user_info(self, info, is_env=False): + "fill out common dirs in user info" + #NOTE: if is_env + HomePath = info.home_dir + + if self.profile == "nt": + #XXX: we could check reg if not is_env + + #check for documents + for docs in ( + filepath(os.environ.get("Documents",None)), #non-standard, used by LiteStep + #FIXME: like to get path from windows registry at this point. + HomePath / "My Documents", + ): + if docs and docs.exists: + info.docs_dir = docs + break + + #XXX: we could check reg if not is_env + + #check for desktop + for desktop in ( + filepath(os.environ.get("Desktop", None)), #non-standard, used by LiteStep + #FIXME: like to get path from windows registry at this point + HomePath / "Desktop", + ): + if desktop and desktop.exists: + info.desktop_dir = desktop + break + + #XXX: fill state_dir w/ app data? + return info + + #----------------------------------------------- + #program related resources + #----------------------------------------------- + def get_service_paths(self, name, login, home): + raise NotImplementError, "no preset paths available for windows services" + + #========================================================= + #EOC + #========================================================= + +#some code I saw for searching through windows processes by name +##cmd = os.popen('query process') +##x = cmd.readlines() +##for y in x: +## p = y.find('openoffice') +## if p >= 0: # process running + +#========================================================= +#registry helpers +#========================================================= +#build map of hkey name -> value +_hkey_values = {} +_hkey_names = {} +def _build_hkey_maps(): + for k in dir(winreg): + if k.startswith("HKEY_"): + s = k[5:].lower() + v = getattr(winreg, k) + _hkey_values[s] = v + _hkey_names[v] = s +_build_hkey_maps() + +_rp_cache_lock = threading.Lock() +_rp_cache = SoftValueDict(expire=300, flush=150) #cache of existing regpaths, keyed by str + +def regpath(path, format=None): + """\ + given a registry path, returns an object which wraps the registry path, + and allows easy introspection of it, in much the same way the :func:`bps.fs.filepath` works. + + This attempts to wrap the windows registry path convention + using a normalized unix-path style, just to make it easier + to refer to and compare various registry paths. + The normalized path format is: + + ``[//{system}]/{root}/...`` + + Where *system* is the name of the system being connected to, + and *root* is the ``HKEY_XXX`` constant, but *without* the ``HKEY_`` prefix, + e.g. ``local_machine`` instead of ``HKEY_LOCAL_MACHINE``. + + .. warning:: + + This function, and the RegPath object, are still a design experiment. + The interface and methods are subject to change or be removed. + """ + #basically this is just a wrapper for RegistryPath, + #except it caches old instances based on the *path* + #this isn't required by the class itself, + #it just saves time & memory + global _rp_cache, _rp_cache_lock + if isinstance(path, RegistryPath) or path is None: + return path + if format == "ms": + #assume it's a backslash separated string, beginning with hkey_xxx + #(ie, the format return by RegistryPath.raw_path) + assert '/' not in path, "not sure how to normalize path with / in name: %r" % (path,) + path = path.replace("\\", "/") + #when presented in this format, we'll allow strings to start with hkey_xxx instead of slash + temp = path.lower() + if not temp.startswith("hkey_"): + raise ValueError, "ms format reg paths must start with HKEY constant: %r" % (path,) + if temp[5:temp.find("/")] not in _hkey_values: + raise ValueError, "ms format reg paths must start with known hkey constant: %r" % (path,) + path = "/" + path + elif format and format != "bps": + raise ValueError, "unknown format: %r" % (format,) + _rp_cache_lock.acquire() + try: + if path in _rp_cache: + return _rp_cache[path] + _rp_cache.flush() + obj = _rp_cache[path] = RegistryPath(path) + return obj + finally: + _rp_cache_lock.release() + +class RegistryPath(BaseClass): + """this represents a path in the registry. + + + This class attempts to mimic the interface for filepaths + provided by :mod:`bps.fs`. + + .. note:: + + This does *not* represent a handle to the registry + (see :class:`RegistryHandle` for that), but it's + various methods may open them. + """ + #========================================================= + #instance attrs + #========================================================= + host = None #name of host, or None if local system + + root = None #name of root key, always defined (lower case - local_machine, current_user, etc) + raw_root = None #name of root key, always defined (upper case with hkey - HKEY_LOCAL_MACHINE, etc) + raw_root_value = None #int value of winreg.HKEY_XXX for root + + subpath = None #subpath relative to root, using "/" as separator + raw_subpath = None #subpath relative to root, using "\\" as separator + + raw_path = None #full path "HKEY_XXX\..." in windows style. + + _rootstr = None #root+host portion of path as a string + _str = None #normalized string of path, returned by __str__ + + #========================================================= + #init + #========================================================= + def __init__(self, path): + #normalize path + orig = path + path = path.lower() + + #parse host + if path.startswith("//"): + idx = path.find("/", 2) + if idx == -1: + host = path[2:] + path = "/local_machine" + else: + host = path[2:idx] + path = path[idx:] + if host.lower() == "localhost": + host = None + else: + host = None + self.host = host + + #parse root + if path.startswith("/"): + parts = path[1:].split("/") + if not parts: + raise ValueError, "no registry root specified in path: %r" % (orig,) + root = parts.pop(0) + else: + raise NotImplementedError, "relative paths not supported: %r" % orig + if root.startswith("hkey_"): + root = root[5:] + if root not in _hkey_values: + raise ValueError, "unknown registry root in path: %r" % orig + self.root = root + self.raw_root = "HKEY_" + root.upper() + self.raw_root_value = _hkey_values[root] + + #parse remaining path + while '' in parts: + parts.remove('') + self.subpath = "/".join(parts) + self.raw_subpath = "\\".join(parts) + + #build full path + if parts: + self.raw_path = self.raw_root + "\\" + self.raw_subpath + else: + self.raw_path = self.raw_root + + #build string + out = "" + if self.host: + out += "//" + self.host + out += "/" + self.root + self._rootstr = out + if parts: + out += "/" + self.subpath + self._str = out + + def __str__(self): + return self._str + + def __repr__(self): + return "regpath(%r)" % self._str + + def __eq__(self, other): + if isinstance(other, RegistryPath): + return self._str == other._str + elif isinstance(other, (str, unicode)): + try: + other = regpath(other) + except NotImplementedError, ValueError: + return False + return self._str == other._str + else: + return False + + #========================================================= + #construction + #========================================================= + def __div__(self, other): + if isinstance(other, (list,tuple)): + if len(other) == 0: + return self + other = "/".join(other) + elif not other: + return self + return regpath(self._str + "/" + other) + + __truediv__ = __div__ + + #'root' stored directly as attribute + + def _getparent(self): + path = self.subpath + if path: + idx = path.rfind("/") + if idx == -1: + return regpath(self._rootstr) + else: + return regpath(self._rootstr + "/" + path[:idx]) + else: + #XXX: return 'self' instead? + return None + parent = property(_getparent) + + def _getname(self): + path = self.subpath + if path: + idx = path.rfind("/") + if idx == -1: + return path + else: + return path[idx+1:] + else: + return self.root + name = property(_getname) + + #========================================================= + #registry tree navigation + #========================================================= + def _getexists(self): + h = self.open(missing='ignore') + if h is None: + return False + else: + h.close() + return True + exists = property(_getexists) + + def _getismissing(self): + return not self.exists + ismissing = property(_getismissing) + + #-------------------------------------------------------- + #proxied from RegHandle object + #-------------------------------------------------------- + def iterdir(self, full=False): + #NOTE: we have to proxy iterator so handle stays open + with self.open() as handle: + for elem in handle.iterdir(full=full): + yield elem + + def listdir(self, full=False): + with self.open() as handle: + return handle.listdir(full=full) + + def _getmtime(self): + with self.open() as handle: + return handle.mtime + mtime = property(_getmtime) + + #TODO: remove() discard() +## def remove(self, recursive=True): + ##winreg.DeleteKey() +## def discard(self, **kwds): +## if self.exists: +## self.remove(**kwds) + + #========================================================= + #registry values + #========================================================= + + #-------------------------------------------------------- + #proxied from RegHandle object + #-------------------------------------------------------- + def iterkeys(self): + #NOTE: we have to proxy iterator so handle stays open + with self.open() as handle: + for key in handle: + yield key + + def keys(self): + with self.open() as handle: + return handle.keys() + + def get(self, key, default=None, missing="ignore", expand=False): + with self.open() as handle: + return handle.get(key, default, missing=missing, expand=expand) + + def get_as_path(self, key, default=None, missing="ignore", expand=True): + with self.open() as handle: + return handle.get_as_path(key, default, missing=missing, expand=expand) + + #========================================================= + #lowlevel access + #========================================================= + def open(self, mode="r", missing='error'): + "return a raw winreg handle" + try: + return reghandle(self, mode) + except WindowsError, err: + if missing == 'ignore' and err.args[0] == 2: #cannot find file specified + return None + else: + raise + + #========================================================= + #eoc + #========================================================= + +#========================================================= +#registry handle (winreg.HKEYType replacement) +#========================================================= +def reghandle(path, mode="r"): + """given a registry path, as accepted by :func:`regpath`, return a handle + to it opened with the specified mode. + """ + return RegistryHandle(path, mode) + +class RegistryHandle(BaseClass): + """replacment for winreg.HKEYType. + + * uses close() instead of Close(), etc + * provides context manager support + * provides regpath attribute to retreive original path + * provides closed attribute + + * supports file()-like mode strings in addition to registry constants. + + ======= ================================================== + Mode Description + ------- -------------------------------------------------- + ``r`` all read perms, equivalent to KEY_READ + + ``w`` all write perms, equivalent to KEY_WRITE + + ``rw`` all read & write permissions + + ``*`` all permissions, equivlanet to KEY_ALL_ACCESS. + for most uses, ``rw`` will be sufficient + ======= ================================================== + """ + #XXX: if there was a way to close a detached handle int, + # we wouldn't have to keep _hkey around and proxy it + _hkey = None #internal HKey we're using + _rkey = None #key to root of remote registry, if host is defined and subpath exists + + def __init__(self, path, mode="r"): + self.path = path = regpath(path) + if not mode: + mode = "r" + if isinstance(mode, int): #allow user to pass in raw winreg.KEY_* values + self.mode = self.mode_value = mode + elif isinstance(mode, str): + if any(c not in "*rw" for c in mode): + raise ValueError, "unknown characters in mode: %r" % (mode,) + mstr = '' + mval = 0 + if '*' in mode: + mstr = '*' + mval = winreg.KEY_ALL_ACCESS + else: + if 'r' in mode: + mstr += 'r' + mval |= winreg.KEY_READ + if 'w' in mode: + mstr += 'w' + mval |= winreg.KEY_WRITE + if not mval: + raise ValueError, "no mode specified: %r" (mode,) + self.mode = mstr + self.mode_value = mval + else: + raise TypeError, "mode must be str or int: %r" % (mode,) + log.debug("opening registry key: path=%r mode=%r", path, self.mode or self.mode_value) + if path.host: + #would probably want to keep a cache of the registry connections, + #so that we can re-use them + h = winreg.ConnectRegistry(path.host, path.raw_root_value) + if path.raw_subpath: + try: + self._hkey = winreg.OpenKey(h, path.raw_subpath, 0, self.mode_value) + finally: + h.Close() #FIXME: is this right? haven't tested this far + else: + self._hkey = h + else: + self._hkey = winreg.OpenKey(path.raw_root_value, path.raw_subpath, 0, self.mode_value) + + def detach(self): + return self._hkey.Detach() + Detach = detach #for backwards compat + + def close(self): + return self._hkey.Close() + Close = close #for backwards compat + + def _gethandle(self): + return self._hkey.handle + handle = property(_gethandle) + + def _getclosed(self): + return self._hkey.handle == 0 + closed = property(_getclosed) + + def __enter__(self): + return self + + def __exit__(self, v, c, t): + self.close() + + def iterdir(self, full=False): + h = self.handle + idx = 0 + try: + while True: + child = winreg.EnumKey(h, idx) + if full: + yield self.path / child + else: + yield child + idx += 1 + except WindowsError, err: + if err.args[0] == 259: #no more data available + return + raise + + def listdir(self, **kwds): + return list(self.iterdir(**kwds)) + + def __iter__(self): + return self.iterkeys() + + def iterkeys(self): + "iterate through all keys under this path" + h = self.handle + idx = 0 + try: + while True: + yield winreg.EnumValue(h, idx)[0] + idx += 1 + except WindowsError, err: + if err.args[0] == 259: #no more data available + return + raise + + def keys(self): + return list(self.iterkeys()) + + def iteritems(self): + for key in self: + yield key, self.get(key) + + def items(self): + return list(self.iteritems()) + + def __contains__(self, key): + try: + winreg.QueryValueEx(self.handle, key) + return True + except WindowsError, err: + if err.args[0] == 2: #file not found + return False + raise + + def __getitem__(self, key): + return self.get(key, missing="error") + + def __setitem__(self, key, value): + return self.set(key, value) + + def __delitem__(self, key): + try: + winreg.DeleteValue(self, key) + except WindowsError, err: + if err.args[0] == 2: #file not found + raise KeyError, "key not found: %r" % key + raise + + def set(self, key, value, type=None): + #NOTE: this is not final call syntax + if type is None: + #TODO: this auto-type detection code could be more intelligent + if value is None: + type = winreg.REG_NONE + elif isinstance(value, (int, long)): + type = winreg.REG_DWORD + #XXX: we _could_ check previous stored type + elif isinstance(value, (str, unicode)): + if '\x00' in value: + type = winreg.REG_BINARY + else: + type = winreg.REG_SZ + else: + raise TypeError, "can't guess type from value: %r" % (value,) + #XXX: should we add some sanity checking here to make sure values match? + winreg.SetValueEx(self.handle, key, None, type, value) + + def raw_get(self, key, default=None, missing="ignore"): + "raw get: returns ``(value,dt)`` or ``(default,None)`` if key is missing" + assert missing in ("ignore", "error") + #NOTE: this is not final call syntax + try: + return winreg.QueryValueEx(self.handle, key) + except WindowsError, err: + if err.args[0] == 2: #file not found + if missing == "ignore": + return default, None + else: + raise KeyError, "key not found in registry: path=%r key=%r" % (self.path, key) + raise + + def get(self, key, default=None, missing="ignore", expand=False): + value, dt = self.raw_get(key, missing=missing) + if dt is None: + return default + if dt in (winreg.REG_SZ, winreg.REG_BINARY): + assert isinstance(value, (str, unicode)), value + return value + elif dt == winreg.REG_EXPAND_SZ: + assert isinstance(value, (str, unicode)), value + if expand and value: + value = _ExpandEnvironmentStrings(value) + return value + elif dt in (winreg.REG_DWORD, winreg.REG_DWORD_BIG_ENDIAN, + winreg.REG_DWORD_LITTLE_ENDIAN): + assert isinstance(value, (int, long)), value + return value + elif dt == winreg.REG_NONE: + #err, is that what this means? + assert value is None, value + return None + else: + #LINK, MULTI_SZ are the known ones we haven't implemented + raise NotImplementedError, "registry type not implemented: %r %r" % (value, dt) + + def get_as_path(self, key, default=None, expand=True, **kwds): + value = self.get(key, default, expand=expand, **kwds) + return filepath(value) + + def get_as_pathlist(self, key, default=None, expand=True, **kwds): + value = self.get(key, default, expand=expand, **kwds) + if value: + return [ filepath(elem) for elem in value.split(os.path.pathsep)] + else: + return [] + + def _getmtime(self): + subfiles, subkeys, wmtime = winreg.QueryInfoKey(self.handle) + if not wmtime: + return 0 + #wmtime - int of 100s of nanoseconds since Jan 1, 1600 + #this converts to epoch... constant at end is ~ 369.24 years, + #derived by setting reg key, and comparing mtime to time.time() when call returned. + return wmtime * 1.0e-7 - 11644473600 + mtime = property(_getmtime) + +#========================================================= +#ms office helpers +#========================================================= +def _hid(): + "attempt at a host id" + return regpath("/local_machine/software/microsoft/windows/currentversion").get("ProductId") + +def detect_outlook(): + "detect outlook version & root" + return detect_office_app("outlook") + +def detect_outlook_express(): + "detect outlook express version & root" + if regpath("/local_machine").ismissing: + log.warning("couldn't connect to windows registry") + return None + outlook = regpath("/local_machine/software/microsoft/outlook express") + if outlook.ismissing: + log.info("microsoft outlook express not found in registry") + return None + path = outlook.get_as_path("InstallRoot") + if not path: + log.warning("outlook express %r has bad InstallRoot") + return None + if path.ismissing: + log.info("outlook express install path missing: %r", path) + return None + vstr = outlook.get("MediaVer") + if vstr: + version=tuple(int(v) for v in vstr.split(",")) + else: + version = None + return dict( + vstr=vstr, + version=version, + path=path, + ) + +def detect_office_app(app): + """detect ms office application, returning path to exe of newest version. + + *app* should be one of ``outlook``, ``word``, ``excel``... er, what others? + + .. note:: + + Whatever happens to the registry interface code, + this function's interface should remain constant. + + .. todo:: + + This could probably be expanded to return a lot more of the info it can gather. + """ + #make sure registry is there... + if regpath("/local_machine").ismissing: + log.warning("couldn't connect to windows registry") + return None + #check for office... + office = regpath("/local_machine/software/microsoft/office") + if office.ismissing: + log.info("microsoft office not found in registry") + return None + best = None #best we're found + for vstr in office.iterdir(): + try: + version = float(vstr) + except ValueError: + #dir should contain just office version numbers, + #but also contains some dirs named "Common" and "Dispatch" + continue + log.debug("found microsoft office version %r", vstr) + if best and version < best['version']: #skip older versions + continue + install = office / vstr / app / "installroot" + if install.ismissing: + log.debug("%s %r not installed", app, vstr) + continue + path = install.get_as_path("path") + if not path: + log.warning("%s %r has bad InstallRoot", app, vstr) + if path.ismissing: + log.info("%s %r install path missing: %r", app, vstr, path) + continue + best = dict(version=version, vstr=vstr, path=path) + log.info("%s %r found at %r", app, vstr, path) + return best + +#========================================================= +#EOC +#========================================================= |
