summaryrefslogtreecommitdiff
path: root/bps/host/base.py
diff options
context:
space:
mode:
Diffstat (limited to 'bps/host/base.py')
-rw-r--r--bps/host/base.py631
1 files changed, 631 insertions, 0 deletions
diff --git a/bps/host/base.py b/bps/host/base.py
new file mode 100644
index 0000000..47560ff
--- /dev/null
+++ b/bps/host/base.py
@@ -0,0 +1,631 @@
+"""bps.host.base -- template for all bps.host implementations.
+
+This module provides the template for all bps.host implementations,
+specifying abstract / default implementations for all the functions
+they should provide, as well as internal helper methods.
+
+TODO: under unix, if we're run as root or as user w/o home dir, should probably store state in /var
+TODO: find_exe needs to deal w/ CWD
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+from logging import getLogger
+import os
+import subprocess
+import sys
+from warnings import warn
+import time
+#pkg
+from bps.types import BaseClass
+from bps.fs import filepath, posix_to_local
+from bps.host.const import DESKTOPS
+from bps.meta import abstractmethod
+from bps.warndep import deprecated_method
+#local
+log = getLogger(__name__)
+
+#=========================================================
+#primary interface
+#=========================================================
+class BackendInterface(BaseClass):
+ """this is minimum interface that all backend implementations must adhere to.
+ the names correspond to the ``bps3.host`` functions, and are documented there.
+ """
+ #=========================================================
+ #creation
+ #=========================================================
+ @classmethod
+ def create(cls):
+ "create new backend handler"
+ #NOTE: this is provided so backend classes can return
+ #an instance of another backend class if they wish
+ #(such as PosixBackend detecting and returning a CygywinBackend)
+ return cls()
+
+ #=========================================================
+ #process management
+ #=========================================================
+ def get_pid(self):
+ "wrapper for os.getpid, for symetry"
+ return os.getpid()
+
+ @abstractmethod
+ def terminate_pid(self, pid, retry, kill, timeout):
+ """send hard-kill signal to *pid*,
+ repeating every *retry* seconds if retry defined,
+ handing off to kill_pid after *kill* seconds if defined,
+ and giving up returning False after *timeout* seconds if timeout defined.
+ """
+
+ @abstractmethod
+ def kill_pid(self, pid, retry, timeout):
+ """send hard-kill signal to *pid*,
+ repeating every *retry* seconds if retry defined,
+ and giving up returning False after *timeout* seconds if timeout defined
+ """
+
+ @abstractmethod
+ def has_pid(self, pid):
+ "Return True if the process id *pid* exits, False if it doesn't."
+ #TODO: would like to detect process status (running, waiting, zombie, etc)
+ # as return richer info on request
+
+ #=========================================================
+ #shell interaction
+ #=========================================================
+ exe_exts = None #list of exe extensions used by host
+
+ @abstractmethod
+ def find_exe(self, name, extra_paths, paths=None):
+ "find exe by name in PATH, or return None"
+
+ #=========================================================
+ #desktop interaction
+ #=========================================================
+ def get_desktop_name(self):
+ "return name of desktop environment, one of the DESKTOP_TYPE strings"
+
+ def desktop_open(self, path, action, mimetype):
+ "attempt to open file using specified action via desktop environment"
+
+ #=========================================================
+ #resource discovery
+ #=========================================================
+ @abstractmethod
+ def user_by_login(self, login):
+ "return UserInfo for user w/ matching login"
+
+ @abstractmethod
+ def user_by_uid(self, uid):
+ "return UserInfo for user w/ matching uid"
+
+ @abstractmethod
+ def get_env_paths(self):
+ "return UserInfo built from current environment"
+
+ @abstractmethod
+ def get_app_paths(self, name):
+ "return ProgPaths for application"
+
+ @abstractmethod
+ def get_service_paths(self, name, login, home):
+ "return ProgPaths for service"
+
+ #=========================================================
+ #EOC
+ #=========================================================
+
+#=========================================================
+#helper classes
+#=========================================================
+class _Info(BaseClass):
+ def __init__(self, **kwds):
+ for k, v in kwds.iteritems():
+ if k.endswith("_file") or k.endswith("_dir") or k.endswith("_path"):
+ v = filepath(v)
+ setattr(self, k, v)
+
+class UserProfile(_Info):
+ """This class represents all the information about a given user account,
+ as returned by :func:`find_user`.
+
+ All :class:`UserProfile<>` instances will have the following attributes:
+
+ .. attribute:: login
+
+ The login name of the user.
+
+ .. attribute:: name
+
+ The display name of the user, as a string.
+
+ .. attribute:: home_dir
+
+ Path to the user's home directory.
+ Should always be defined & exist.
+
+ .. attribute:: desktop_dir
+
+ Path to the user's desktop. Will be defined IFF it exists.
+
+ .. attribute:: docs_dir
+
+ Path to user's documents directory. Will be defined IFF it exists.
+
+ .. NOTE::
+ The logic of this directory's selection is currently a little hackneyed.
+
+ .. attribute:: start_dir
+
+ Chosen from one of the above, this should always be a good directory
+ to open a file browser into.
+
+ .. attribute:: state_dir
+
+ Directory applications should use to store persistent application state.
+ This uses ``APPDATA`` under windows, and ``~/.config`` under posix.
+
+ These attributes will only be defined under a ``posix`` environment,
+ they will be set to ``None`` for all others:
+
+ .. attribute:: uid
+
+ Integer uid assigned to account.
+
+ .. attribute:: gid
+
+ Integer gid assigned to account's primary group.
+
+ .. attribute:: shell_file
+
+ Path to user's default shell.
+ """
+ #=========================================================
+ #os independant
+ #=========================================================
+
+ #-----------------------------------------------
+ #user stats
+ #-----------------------------------------------
+ name = None #display name of user
+ login = None #login name of user
+
+ #-----------------------------------------------
+ #resource paths
+ #-----------------------------------------------
+ home_dir = None #path to home directory (should always be defined & exist)
+ desktop_dir = None #path to desktop directory (should always be defined IF exists)
+ docs_dir = None #path to user's documents (should always be defined IF exists)
+
+ #path filebrowsers should start in (should always be defined)
+ def _get_start_dir(self):
+ return self.desktop_dir or self.home_dir
+ start_dir = property(_get_start_dir)
+
+ #-----------------------------------------------
+ #app info helpers
+ #-----------------------------------------------
+ #XXX: could list some desktop stuff here
+ state_dir = None #path where apps should store config (AppData under win32, .config under posix)
+
+ #=========================================================
+ #posix
+ #=========================================================
+ uid = None #uid of user
+ gid = None #uid of user's primary group
+
+ shell_file = None #path to user's default shell
+
+ #=========================================================
+ #win32
+ #=========================================================
+
+ #=========================================================
+ #EOC
+ #=========================================================
+
+class EnvPaths(_Info):
+ """This class represents all the information about the current environment's
+ resource paths, as returned by :func:`get_env_paths`. It should not be instantiated directly.
+ Any values not defined under the current environment will be set to `None`.
+
+ All :class:`EnvPaths<>` instances will have the following attributes:
+
+ .. attribute:: login
+
+ The login name of the user account we were run from.
+ May not always be defined.
+
+ .. attribute:: home_dir
+
+ Path to the user's home directory.
+ Will always be present, unless your script is being run from ``/etc/init.d``.
+
+ .. attribute:: desktop_dir
+
+ Path to the user's desktop.
+ Will be defined if and only if it exists.
+
+ .. attribute:: docs_dir
+
+ Path to user's documents directory.
+ Will be defined if and only if it exists.
+
+ .. warning::
+ The logic of this directory's selection is currently a little hackneyed
+ under posix.
+
+ .. attribute:: start_dir
+
+ Chosen from one of the above, this should always be a good directory
+ to open a file browser into.
+
+ .. attribute:: state_dir
+
+ Directory applications should use to store configuration.
+ This uses ``%APPDATA%`` under windows, and ``~/.config`` under posix.
+
+ The following attributes will only be defined for posix,
+ they will be set to ``None`` for all all other OSes:
+
+ .. attribute:: shell_file
+
+ Path to the shell we were run under.
+
+ .. note::
+
+ This class contains a subset of same attributes as :class:`UserProfile`,
+ but the contents of this class are derived from ``os.environ``, whereas
+ the contents of that class are derived from the host's user account database.
+ Thus, while they will frequently be in agreement, this is not a guarantee.
+ """
+ #=========================================================
+ #os independant
+ #=========================================================
+
+ #-----------------------------------------------
+ #user stats
+ #-----------------------------------------------
+ login = None #login name of user
+
+ #-----------------------------------------------
+ #resource paths
+ #-----------------------------------------------
+ home_dir = None #path to home directory (should always be defined & exist)
+ desktop_dir = None #path to desktop directory (should always be defined IF exists)
+ docs_dir = None #path to user's documents (should always be defined IF exists)
+
+ #path filebrowsers should start in (should always be defined)
+ def _get_start_dir(self):
+ return self.desktop_dir or self.home_dir
+ start_dir = property(_get_start_dir)
+
+ #-----------------------------------------------
+ #app info helpers
+ #-----------------------------------------------
+ #XXX: could list some desktop stuff here
+ state_dir = None #path where apps should store config (AppData under win32, .config under posix)
+
+ #=========================================================
+ #posix
+ #=========================================================
+ shell_file = None #path to user's default shell
+
+ #=========================================================
+ #win32
+ #=========================================================
+
+ #=========================================================
+ #EOC
+ #=========================================================
+
+class ProgPaths(_Info):
+ """This class is used to hold the results of a :func:`get_app_paths` call.
+ See that function for more details.
+
+ Each :class:`ProgInfo<>` object contains the following attributes:
+
+ .. attribute:: name
+
+ The name of the application, as passed into :func:`get_app_paths`,
+ after being normalized for host naming conventions.
+
+ .. attribute:: state_dir
+
+ The application may use this directory to store any persistent data
+ which should be kept between invocations of the application,
+ and should survive system reboots, etc.
+
+ Under windows, this will point to the ``%APPDATA%/{name}`` directory,
+ and under posix, this will point to ``~/.config/{name}``
+
+ .. note::
+ For services, this will generally be a read-only directory, eg ``/etc``
+
+ .. attribute:: run_dir
+
+ The application may use this directory to store any data
+ which does not need to persist past the current invocation of the program.
+ Normally, this is set to the same value as ``state_dir``.
+
+ .. attribute:: cache_dir
+
+ Directory to stored cached data. Usually defaults to ``{state_dir}/cache``.
+
+ .. attribute:: lock_file
+
+ Recommending location for application's lock file.
+ Usually defaults to ``{run_dir}/{name}.lock``
+
+ For profiles generated by :func:`get_service_paths`, the following
+ attributes will also be set:
+
+ .. attribute:: config_dir
+
+ This should point to (usually read-only) default configuration
+ for the service. Eg, this is ``/etc/{name}`` under posix.
+
+ .. attribute:: log_file
+
+ Suggested log file for service.
+ """
+ name = None #name of application (used to fill in some paths)
+ first_name = None
+
+ state_dir = None #path to persistent state directory
+ run_dir = None #path to run-time state directory
+ cache_dir = None #path to cache directory (usually state_dir / cache)
+ lock_file = None #path to lock file (usually run_dir / pid.lock)
+
+ config_dir = None #path to config directory
+ log_file = None #path to log file
+
+#=========================================================
+#base backend class
+#=========================================================
+class BaseBackend(BackendInterface):
+ "base backend class which provides helpers needed by most implementations"
+ #=========================================================
+ #class attrs
+ #=========================================================
+ pid_check_refresh = .1 #delay in terminate/kill_pid loop
+
+ #=========================================================
+ #instance attrs
+ #=========================================================
+
+ #desktop interaction attrs
+ desktop_loaded = False #set to True when desktop discover run
+ desktop_name = None #desktop name when loaded
+
+ #resource discovery attrs
+ resources_loaded = False
+ env = None #EnvProfile filled out by load_resources
+
+ #=========================================================
+ #creation
+ #=========================================================
+
+ def __init__(self, **kwds):
+ self.__super.__init__(**kwds)
+ if not isinstance(self.exe_exts, tuple):
+ self.exe_exts = tuple(self.exe_exts)
+
+ #=========================================================
+ #process management
+ #=========================================================
+ def terminate_pid(self, pid, retry, kill, timeout):
+ """wraps _terminate_pid() and provides the complex behavior host.terminate_pid requires"""
+ if not self._terminate_pid(pid):
+ return True
+ now = time.time()
+ retry_after = now + retry if retry else None
+ kill_after = now + kill if kill else None
+ timeout_after = now + timeout if timeout and timeout > kill else None
+ delay = self.pid_check_refresh
+ while True:
+ time.sleep(delay)
+ if not self.has_pid(pid):
+ return True
+ now = time.time()
+ if retry_after and retry_after <= now:
+ if not self._terminate_pid(pid):
+ return True
+ retry_after = now + retry
+ if kill_after and kill_after <= now:
+ #NOTE: we decrease timeout since it measures TOTAL amount of time spent signalling.
+ if timeout:
+ if timeout > kill:
+ timeout -= kill
+ else:
+ log.warning("terminate_pid(): timeout window less than kill window: %r vs %r", timeout, kill)
+ timeout = 30
+ return self.kill_pid(pid, retry, timeout)
+ if timeout_after and timeout_after <= now:
+ return False
+
+ def kill_pid(self, pid, retry, timeout):
+ """wraps _kill_pid() and provides the complex behavior host.kill_pid requires"""
+ if not self._kill_pid(pid):
+ return True
+ now = time.time()
+ retry_after = now + retry if retry else None
+ timeout_after = now + timeout if timeout else None
+ delay = self.pid_check_refresh
+ while True:
+ time.sleep(delay)
+ if not self.has_pid(pid):
+ return True
+ now = time.time()
+ if retry_after and retry_after <= now:
+ if not self._kill_pid(pid):
+ return True
+ retry_after = now + retry
+ if timeout_after and timeout_after <= now:
+ return False
+
+ #---------------------------------------------------------
+ #subclass interface
+ #---------------------------------------------------------
+ @abstractmethod
+ def _terminate_pid(self, pid):
+ "helper to terminate specified process"
+ #returns True if signal sent, False if pid not found (ala has_pid)
+
+ @abstractmethod
+ def _kill_pid(self, pid):
+ "helper to kill specified process"
+ #returns True if signal sent, False if pid not found (ala has_pid)
+
+ #=========================================================
+ #shell interaction
+ #=========================================================
+ def find_exe(self, name, extra_paths=None, paths=None):
+ "scan host os's exe path for binary with specified name"
+ #NOTE: for most OS's, this won't need to be overridden.
+ if paths is None:
+ paths = self.get_exe_paths()
+ elif extra_paths:
+ paths = list(paths) #don't let extra paths modify original
+ if extra_paths:
+ paths.extend(extra_paths)
+ #FIXME: should CWD be included?
+ for prefix in paths:
+ #XXX: will expandvars work right under nt?
+ prefix = os.path.expanduser(os.path.expandvars(prefix))
+ for ext in self.exe_exts:
+ path = os.path.join(prefix, "%s%s" % (name, ext))
+ if os.path.exists(path):
+ return filepath(path)
+ return None
+
+ def get_exe_paths(self):
+ "[find_exe helper] return list of paths to check"
+ #XXX: this strips null directories out... should they be treated as '.', or ignored?
+ return [ path for path in os.environ["PATH"].split(os.path.pathsep) if path ]
+
+ #=========================================================
+ #desktop interaction
+ #=========================================================
+
+ #---------------------------------------------------------
+ #desktop discovery helpers
+ #---------------------------------------------------------
+ def init_desktop(self):
+ "delays desktop discovery if needed"
+ if not self.desktop_loaded:
+ self.load_desktop()
+ assert self.desktop_name in DESKTOPS
+ self.desktop_loaded = True
+
+ def load_desktop(self):
+ self.desktop_name = self.detect_desktop()
+
+ def detect_desktop(self):
+ "helper to detect what type of desktop environment is in use, returning id string or None"
+ get = os.environ.get
+ has = os.environ.__contains__
+
+ #check for windows desktop
+ if os.name == "nt" or os.name == "ce":
+ return "windows"
+
+ #check for osx
+ if os.name == "osx":
+ return "osx"
+
+ #check for X11 environments
+ if os.environ.get("DISPLAY"):
+ ds = get("DESKTOP_SESSION")
+
+ #check for kde
+ #XXX: we could check for kde4 before windows/macosx
+ if ds == "kde" or has('KDE_SESSION_UID') or has('KDE_FULL_SESSION'):
+ #XXX: distinguish kde3 / kde4?
+ return "kde"
+
+ #check for gnome
+ if ds == "gnome" or has('GNOME_SESSION_ID'):
+ return "gnome"
+
+ #check for xfce
+ if self.find_exe("xprop"):
+ #XXX: is there a easier way to detect xfce?
+ p = subprocess.Popen(['xprop', '-root', '_DT_SAVE_MODE'], stdout=subprocess.PIPE)
+ p.wait()
+ v = p.stdout.read().strip()
+ if v == '_DT_SAVE_MODE(STRING) = "xfce4"':
+ return "xfce"
+
+ #give up
+ return None
+
+ #---------------------------------------------------------
+ #interface helpers
+ #---------------------------------------------------------
+ def get_desktop_name(self):
+ #subclasses shouldn't need to override this (tweak detect_desktop instead)
+ self.init_desktop()
+ return self.desktop_name
+
+ def desktop_open(self, path, action, mimetype):
+ #subclasses will want to override this, thus is just a stub
+ #they will also want to call self.init_desktop()
+ return self.stub_open(path, action, mimetype)
+
+ def stub_open(self, path, action, mimetype):
+ "stub for when no opener is available"
+ warn("stub desktop open(): file not opened: path=%r action=%r mimetype=%r" %
+ (path, action, mimetype))
+ return None
+
+ #=========================================================
+ #resource discovery
+ #=========================================================
+ #NOTE: this code assumed env profile is built up once, and used by other bits
+
+ def init_resources(self):
+ if not self.resources_loaded:
+ self.load_resources()
+ self.resources_loaded = True
+
+ def load_resources(self):
+ self.env = EnvPaths()
+
+ def get_env_paths(self):
+ "return EnvPaths instance"
+ self.init_resources()
+ return self.env
+
+ def norm_prog_name(self, name):
+ "helper for normalized program names"
+ name = name.replace(" ", "-")
+ if '/' in name:
+ tail = name.rsplit("/", 1)[1]
+ name = posix_to_local(name)
+ else:
+ tail = name
+ return name, tail
+
+ def get_app_paths(self, name):
+ "standard app path creator, works for windows and posix"
+ env = self.get_env_paths()
+ name, tail = self.norm_prog_name(name)
+ state_dir = run_dir = env.state_dir / name
+ return ProgPaths(
+ name=name, first_name=tail,
+ state_dir=state_dir,
+ run_dir=run_dir,
+ cache_dir=state_dir / "cache",
+ lock_file=run_dir / (tail + ".pid"),
+ )
+
+ #=========================================================
+ #EOC
+ #=========================================================
+
+#=========================================================
+#EOC
+#=========================================================