finished argument parsing and dealing with procs and sockets in FollowContextManager
authorChristian Herdtweck <christian.herdtweck@intra2net.com>
Thu, 1 Oct 2015 16:09:38 +0000 (18:09 +0200)
committerChristian Herdtweck <christian.herdtweck@intra2net.com>
Thu, 1 Oct 2015 16:09:38 +0000 (18:09 +0200)
follow.py

index f113f71..5cdd6ad 100644 (file)
--- a/follow.py
+++ b/follow.py
@@ -95,6 +95,8 @@ NOT possible::
 from __future__ import print_function
 from warnings import warn
 import select
+from subprocess import Popen, PIPE
+from type_helpers import isstr
 
 # #############################################################################
 # CONSTANTS
@@ -226,7 +228,7 @@ class Follower(object):
             sources_and_descriptions = sources_and_descriptions[0]
 
         for arg in sources_and_descriptions:
-            if isinstance(arg, str):  # is a description
+            if isstr(arg):  # is a description
                 if len(descriptions) == 0:
                     raise ValueError(
                         'first arg must be source, not a description!')
@@ -300,6 +302,8 @@ class Follower(object):
 # CONTEXT MANAGEMENT
 # #############################################################################
 
+#: defualt args for Popen constructor in case a process is given as cmd
+DEFAULT_SUBPROCESS_ARGS = dict(bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE)
 
 class FollowContextManager(object):
     """ context for Follower objects, ensures everything is closed properly
@@ -317,60 +321,164 @@ class FollowContextManager(object):
 
     procs = None
     proc_descs = None
+    proc_objs = None
 
     def __init__(self, files=None, file_descs=None,
                  sockets=None, socket_descs=None,
-                 procs=None, proc_descs=None):
-        """ create a context manager for Follower, only does arg checking """
+                 procs=None, proc_descs=None,
+                 subprocess_args=None):
+        """ create a context manager for Follower
+
+        check args and that they match.
+        tries to guess good descs for files, sockets and procs that are not
+        given
+
+        :param files: list/tuple of, or single file handle or file name
+        :param file_descs: None or list/tuple of same length as files
+        :param sockets: list/tuple of, or single socket
+        :param socket_descs: None or list/tuple of same length as sockets
+        :param procs: list/tuple of, or single Popen object or command itself
+                      as str/list/tuple
+        :param proc_descs: None or list/tuple of same length as procs
+        :param dict subprocess_args: dict or args that are merged with
+                                     :py:data:`DEFAULT_SUBPROCESS_ARGS`
+        """
         print('__init__')
 
+        # set files and file_descs and ensure that they matching lists
         if files is None:
             self.files = []
-        if isinstance(files, str):
+        elif isstr(files):
+            self.files = [files, ]
+        elif isinstance(files, file):
             self.files = [files, ]
         else:
             self.files = files
         if file_descs is None:
-            self.file_descs = [None for _ in self.files]
-        elif len(self.files) == len(self.file_descs):
-            self.file_descs = file_descs
+            temp_descs = [None for _ in self.files]
+        elif len(self.files) == len(file_descs):
+            temp_descs = file_descs
         else:
             raise ValueError('if given file descs, need desc for all files!')
 
+        # try to guess good file_desc values; ensure they are str
+        self.file_descs = []
+        for file_nr, (file, file_desc) in \
+                enumerate(zip(self.files, temp_descs)):
+            if isstr(file_desc):
+                self.file_descs.append(file_desc)
+                continue
+            elif file_desc is None:   # need to guess something
+                if isstr(file):
+                    self.file_descs.append(file)
+                else:
+                    self.file_descs.append('file{0}'.format(file_nr))
+            else:   # desc is neither str nor None
+                raise ValueError('file descs must be string or None!')
+
+        # set sockets and socket_descs and ensure that they matching lists
         if sockets is None:
             self.sockets = []
+        elif isinstance(sockets, Socket):
+            self.sockets = [sockets, ]
         else:
             self.sockets = sockets
-        self.socket_descs = socket_descs
+        if socket_descs is None:
+            temp_descs = [None for _ in self.sockets]
+        elif len(self.sockets) == len(socket_descs):
+            temp_descs = socket_descs
+        else:
+            raise ValueError('if given socket descs, '
+                             'need descs for all sockets!')
+
+        # try to guess good socket_desc values; ensure they are str
+        self.socket_descs = []
+        for file_nr, (socket, socket_desc) in \
+                enumerate(zip(self.sockets, temp_descs)):
+            if isstr(socket_desc):
+                self.socket_descs.append(socket_desc)
+            elif socket_desc is None:   # need to guess something
+                self.socket_descs.append('socket{0}'.format(socket_nr))
+            else:   # desc is neither str nor None
+                raise ValueError('socket descs must be string or None!')
+
+        # set procs and proc_descs and ensure they matching lists
         if procs is None:
-            procs = []
+            self.procs = []
+        elif isstr(procs):
+            self.procs = [procs, ]
+        elif isinstance(procs, Popen):
+            self.procs = [procs, ]
         else:
             self.procs = procs
-        self.proc_descs = proc_descs
+        if proc_descs is None:
+            temp_descs = [None for _ in self.procs]
+        elif len(proc_descs) == len(self.procs):
+            temp_descs = proc_descs
+        else:
+            raise ValueError('if given proc descs, need descs for all procs!')
+
+        # try to guess good proc_desc values; ensure they are str
+        self.proc_descs = []
+        for proc_nr, (proc, proc_desc) in \
+                enumerate(zip(self.procs, temp_descs)):
+            if isstr(proc_desc):
+                self.proc_descs.append(proc_desc)
+            elif proc_desc is None:   # need to guess something
+                if isstr(proc):
+                    self.proc_descs.append(proc)
+                elif isinstance(proc, (tuple, list)):
+                    self.proc_descs.append(' '.join(proc))
+                elif isinstance(proc, Popen):
+                    if isstr(proc.args):
+                        self.proc_descs.append(proc.args)
+                    else:
+                        self.proc_descs.append(' '.join(proc.args))
+                else:
+                    self.proc_descs.append('proc{0}'.format(proc_nr))
+            else:   # desc is neither str nor None
+                raise ValueError('proc descs must be string or None!')
+
+        self.subprocess_args = DEFAULT_SUBPROCESS_ARGS
+        if subprocess_args is not None:
+            self.subprocess_args.update(subprocess_args)
 
     def __enter__(self):
-        """ called when entering run context """
+        """ called when entering run context
+
+        opens files, tries to create descs, and assembles args for Follower
+        """
         print('__enter__')
 
         args = []
 
         # open files
         self.file_handles = []
-        for file_arg, new_desc in zip(self.files, self.file_descs):
+        for file_arg, desc in zip(self.files, self.file_descs):
             if isinstance(file_arg, str):
                 new_handle = open(file_arg)
-                if new_desc is None:
-                    new_desc = file_arg
             else:       # assume file_arg is a file handle
                 new_handle = file_arg
-                if new_desc is None:
-                    new_desc = file_arg.name
             self.file_handles.append(new_handle)
             args.append(new_handle)
-            args.append(new_desc)
+            args.append(desc)
         # end: for files and file_descs
 
-        # TODO: repeat for sockets and procs, add them to args
+        for sock_number, (socket, desc) in \
+                enumerate(zip(self.sockets, self.socket_descs)):
+            args.append(socket)
+            args.append(desc)
+
+        self.proc_objs = []
+        for proc, desc in zip(self.procs, self.proc_descs):
+            if isstr(proc) or isinstance(proc, (list,tuple)):
+                proc = Popen(proc, **self.subprocess_args)
+                self.proc_objs.append(proc)
+            args.append(proc.stdout)
+            args.append(desc + '_out')
+            args.append(proc.stderr)
+            args.append(desc + '_err')
+
         return Follower(args)
 
     def __exit__(self, exc_type, exc_value, traceback):
@@ -382,10 +490,16 @@ class FollowContextManager(object):
         for handle in self.file_handles:
             try:
                 handle.close()
-            except:
+            except Exception:
+                warn('ignoring exception exiting follower context manager!')
+        for proc in self.proc_objs:
+            try:
+                proc.kill()
+                rest_out, rest_err = proc.communicate()
+                if rest_out or rest_err:
+                    warn('Ignoring left-over output in proc')
+            except Exception:
                 warn('ignoring exception exiting follower context manager!')
-
-        # TODO: communicate and wait for procs, close sockets
 # end: class FollowContextManager