diff --git a/.gitignore b/.gitignore
index 48488751f453b7dc24fdc1bec38a83868b54d996..4bc4d7fc6fb656527f598779e544316ef09e44e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ dist/
 *.log
 .venv*/
 .tox/
+/coverage/
+/.eggs/
diff --git a/.gitreview b/.gitreview
deleted file mode 100644
index c882297be20922874d66a28df3437eefd06d2875..0000000000000000000000000000000000000000
--- a/.gitreview
+++ /dev/null
@@ -1,8 +0,0 @@
-[gerrit]
-host=gerrit.coast-project.org
-port=29418
-project=sconsider.git
-defaultbranch=master
-defaultremote=origin
-defaultrebase=1
-
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index c3d5982324e598545f0a7b1ce3f990b93fd16554..d521aaafd01a1d10eb437a7c0eb0e2662363d3fc 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -1,7 +1,94 @@
 = Changelog
 
+== tag: 0.3.17
+* Bumped version to 0.3.17 (91de657)
+
+
+* Worked heavily on how to run and control backend processes (bfac068)
+
+
+* expand search directories for source files (90512af)
+
+
+* make it clear that timeout is a float value (2377aff)
+
+
+* fixed runTimeout setting from command line (6d0faf4)
+
+
+* fixed spurious temp file close errors (ae5461d)
+
+
+* handover signals to started process using exec (9876a1b)
+
+
+* extended RunBuilder to use a timeout for the started subprocess (fdef451)
+
+
+* collecting stderr along with stdout messages in RunBuilder (4019dbb)
+
+
+* terminating subprocess before setting internal termination flag (1fd70ab)
+
+
+* using explicit seconds_to_wait for ProcessRunner (958d436)
+
+
+* replaced all PopenHelper classes with PreocessRunner (6a14556)
+
+
+* exchanged PopenHelper with ProcessRunner (7c0a463)
+
+
+* logging test duration (fbfd0b6)
+
+
+* separated stderr from stdout in ProcessRunner (17e9721)
+
+
+* tests added to test basic functionality of ProcessRunner (eba0229)
+
+
+* renamed property to allow easier PopenHelper replacement (9d92db1)
+
+
+* remove writer to close from witers list prior to closing (1276655)
+
+
+* replaced PopenHelper in RunBuilder with ProcessRunner (9284dc2)
+
+
+* new ProcessRunner to replace PopenHelper class (8567d46)
+
+
+* removed unused code sections (b3c6148)
+
+
+* moved Tee to PopenHelper (3a2e58f)
+
+
+* reworked Tee class to be more flexible (2085fdc)
+
+
+* fixed incorrect return code (fb3e09e)
+
+
+* extended Popen logging to show return code (a9640c7)
+
+
+* added xml file header (1f1e4fe)
+
+
+* removed gitreview file (e225f08)
+
+
+* Merge tag '0.3.16' into develop (38805d0)
+
 == tag: 0.3.16
-* updated changelog
+* fixed DoxygenBuilder by using correct array by reference modification (44b2d9f)
+
+
+* updated changelog (cec2fc5)
 
 
 * Bumped version to 0.3.16 (718be28)
diff --git a/SConsider/LibFinder.py b/SConsider/LibFinder.py
index e0177f7fe9a9554c7911e46c00f4f1ce833499b8..1e22d85405ed966f3f3758b9db04166819d38dda 100644
--- a/SConsider/LibFinder.py
+++ b/SConsider/LibFinder.py
@@ -19,7 +19,7 @@ import functools
 import itertools
 import operator
 from SConsider.SomeUtils import getFlatENV
-from SConsider.PopenHelper import PopenHelper, PIPE
+from SConsider.PopenHelper import PopenHelper, ProcessRunner
 
 
 def uniquelist(iterable):
@@ -71,15 +71,17 @@ class UnixFinder(LibFinder):
     def getLibs(self, env, source, libnames=None, libdirs=None):
         if libdirs:
             env.AppendENVPath('LD_LIBRARY_PATH', [self.absolutify(j) for j in libdirs])
-        ldd = PopenHelper(['ldd', os.path.basename(source[0].get_abspath())],
-                          stdout=PIPE,
-                          cwd=os.path.dirname(source[0].get_abspath()),
-                          env=getFlatENV(env))
-        out, _ = ldd.communicate()
-        libs = [
-            j for j in re.findall(r'^.*=>\s*(not found|[^\s^\(]+)', out, re.MULTILINE)
-            if functools.partial(operator.ne, 'not found')(j)
-        ]
+        libs = []
+        cmd = ['ldd', os.path.basename(source[0].get_abspath())]
+        with ProcessRunner(cmd,
+                           timeout=30,
+                           seconds_to_wait=0.1,
+                           cwd=os.path.dirname(source[0].get_abspath()),
+                           env=getFlatENV(env)) as executor:
+            for out, _ in executor:
+                for j in re.findall(r'^.*=>\s*(not found|[^\s^\(]+)', out, re.MULTILINE):
+                    if functools.partial(operator.ne, 'not found')(j):
+                        libs.append(j)
         if libnames:
             libs = [j for j in libs if functools.partial(self.__filterLibs, env, libnames=libnames)(j)]
         return libs
@@ -89,16 +91,16 @@ class UnixFinder(LibFinder):
         linkercmd = env.subst('$LINK')
         if not linkercmd:
             return libdirs
-        cmdargs = [linkercmd, '-print-search-dirs'] + env.subst('$LINKFLAGS').split(' ')
-        linker = PopenHelper(cmdargs, stdout=PIPE, env=getFlatENV(env))
-        out, _ = linker.communicate()
-        match = re.search('^libraries.*=(.*)$', out, re.MULTILINE)
-        if match:
-            libdirs.extend(
-                unique([
-                    os.path.abspath(j) for j in match.group(1).split(os.pathsep)
-                    if os.path.exists(os.path.abspath(j))
-                ]))
+        cmd = [linkercmd, '-print-search-dirs'] + env.subst('$LINKFLAGS').split(' ')
+        with ProcessRunner(cmd, timeout=30, env=getFlatENV(env)) as executor:
+            for out, _ in executor:
+                match = re.search('^libraries.*=(.*)$', out, re.MULTILINE)
+                if match:
+                    libdirs.extend(
+                        unique([
+                            os.path.abspath(j) for j in match.group(1).split(os.pathsep)
+                            if os.path.exists(os.path.abspath(j))
+                        ]))
         return libdirs
 
 
@@ -120,15 +122,19 @@ class MacFinder(LibFinder):
     def getLibs(self, env, source, libnames=None, libdirs=None):
         if libdirs:
             env.AppendENVPath('DYLD_LIBRARY_PATH', [self.absolutify(j) for j in libdirs])
-        ldd = PopenHelper(['otool', '-L', os.path.basename(source[0].get_abspath())],
-                          stdout=PIPE,
-                          cwd=os.path.dirname(source[0].get_abspath()),
-                          env=getFlatENV(env))
-        out, _ = ldd.communicate()
-        libs = [
-            j for j in re.findall(r'^.*=>\s*(not found|[^\s^\(]+)', out, re.MULTILINE)
-            if functools.partial(operator.ne, 'not found')(j)
-        ]
+
+        libs = []
+        cmd = ['otool', '-L', os.path.basename(source[0].get_abspath())]
+        with ProcessRunner(cmd,
+                           timeout=30,
+                           seconds_to_wait=0.1,
+                           cwd=os.path.dirname(source[0].get_abspath()),
+                           env=getFlatENV(env)) as executor:
+            for out, _ in executor:
+                for j in re.findall(r'^.*=>\s*(not found|[^\s^\(]+)', out, re.MULTILINE):
+                    if functools.partial(operator.ne, 'not found')(j):
+                        libs.append(j)
+
         if libnames:
             libs = [j for j in libs if functools.partial(self.__filterLibs, env, libnames=libnames)(j)]
         return libs
@@ -136,16 +142,17 @@ class MacFinder(LibFinder):
     def getSystemLibDirs(self, env):
         libdirs = []
         linkercmd = env.subst('$LINK')
-        cmdargs = [linkercmd, '-print-search-dirs'] + env.subst('$LINKFLAGS').split(' ')
-        linker = PopenHelper(cmdargs, stdout=PIPE, env=getFlatENV(env))
-        out, _ = linker.communicate()
-        match = re.search('^libraries.*=(.*)$', out, re.MULTILINE)
-        if match:
-            libdirs.extend(
-                unique([
-                    os.path.abspath(j) for j in match.group(1).split(os.pathsep)
-                    if os.path.exists(os.path.abspath(j))
-                ]))
+        cmd = [linkercmd, '-print-search-dirs'] + env.subst('$LINKFLAGS').split(' ')
+
+        with ProcessRunner(cmd, timeout=30, seconds_to_wait=0.1, env=getFlatENV(env)) as executor:
+            for out, _ in executor:
+                match = re.search('^libraries.*=(.*)$', out, re.MULTILINE)
+                if match:
+                    libdirs.extend(
+                        unique([
+                            os.path.abspath(j) for j in match.group(1).split(os.pathsep)
+                            if os.path.exists(os.path.abspath(j))
+                        ]))
         return libdirs
 
 
@@ -165,12 +172,15 @@ class Win32Finder(LibFinder):
         return None
 
     def getLibs(self, env, source, libnames=None, libdirs=None):
-        ldd = PopenHelper(['objdump', '-p', os.path.basename(source[0].get_abspath())],
-                          stdout=PIPE,
-                          cwd=os.path.dirname(source[0].get_abspath()),
-                          env=getFlatENV(env))
-        out, _ = ldd.communicate()
-        deplibs = re.findall(r'DLL Name:\s*(\S*)', out, re.MULTILINE)
+        deplibs = []
+        cmd = ['objdump', '-p', os.path.basename(source[0].get_abspath())]
+        with ProcessRunner(cmd,
+                           timeout=30,
+                           seconds_to_wait=0.1,
+                           cwd=os.path.dirname(source[0].get_abspath()),
+                           env=getFlatENV(env)) as executor:
+            for out, _ in executor:
+                deplibs.extend(re.findall(r'DLL Name:\s*(\S*)', out, re.MULTILINE))
         if not libdirs:
             libdirs = self.getSystemLibDirs(env)
         if libnames:
diff --git a/SConsider/PopenHelper.py b/SConsider/PopenHelper.py
index 9babf780234b13ad625a8c0207ac78298595d9a6..1f75413d1c9f7aba774051ee69d50c0396467cb1 100644
--- a/SConsider/PopenHelper.py
+++ b/SConsider/PopenHelper.py
@@ -9,28 +9,57 @@
 # -------------------------------------------------------------------------
 
 import shlex
+import sys
+import os
+from locale import getpreferredencoding
+import time
+from threading import Timer, Thread, Event
+from tempfile import NamedTemporaryFile
 from logging import getLogger
 
 logger = getLogger(__name__)
 has_timeout_param = True
 try:
-    from subprocess32 import Popen, PIPE, STDOUT, TimeoutExpired
+    from subprocess32 import Popen, PIPE, STDOUT, TimeoutExpired, CalledProcessError
 except:
     try:
-        from subprocess import Popen, PIPE, STDOUT, TimeoutExpired
+        from subprocess import Popen, PIPE, STDOUT, TimeoutExpired, CalledProcessError
     except:
         from subprocess import Popen, PIPE, STDOUT
 
+        # Exception classes copied over from subprocess32
+        class CalledProcessError(Exception):
+            """This exception is raised when a process run by check_call() or
+            check_output() returns a non-zero exit status.
+
+            The exit status will be stored in the returncode attribute;
+            check_output() will also store the output in the output
+            attribute.
+            """
+            def __init__(self, returncode, cmd, output=None):
+                self.returncode = returncode
+                self.cmd = cmd
+                self.output = output
+
+            def __str__(self):
+                return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
+
         class TimeoutExpired(Exception):
-            def __init__(self):
-                pass
+            """This exception is raised when the timeout expires while waiting
+            for a child process."""
+            def __init__(self, cmd, timeout, output=None):
+                self.cmd = cmd
+                self.timeout = timeout
+                self.output = output
+
+            def __str__(self):
+                return ("Command '%s' timed out after %s seconds" % (self.cmd, self.timeout))
 
         has_timeout_param = False
 
 
 class PopenHelper(object):
     def __init__(self, command, **kw):
-        self.os_except = None
         self.returncode = None
         self.has_timeout = has_timeout_param
         _exec_using_shell = kw.get('shell', False)
@@ -40,11 +69,12 @@ class PopenHelper(object):
         logger.debug("Executing command: %s with kw-args: %s", _command_list, kw)
         self.po = Popen(_command_list, **kw)
 
-    def communicate(self, stdincontent=None, timeout=10, raise_except=False):
+    def communicate(self, stdincontent=None, timeout=10):
         _kwargs = {'input': stdincontent}
         if self.has_timeout:
             _kwargs['timeout'] = timeout
         try:
+            logger.debug("communicating with _kwargs: %s", _kwargs)
             _stdout, _stderr = self.po.communicate(**_kwargs)
         except TimeoutExpired:
             _kwargs = {'input': None}
@@ -53,12 +83,201 @@ class PopenHelper(object):
             self.po.kill()
             _stdout, _stderr = self.po.communicate(**_kwargs)
         except OSError as ex:
-            self.os_except = ex
+            # raised if the executed program cannot be found
+            logger.debug(ex)
         finally:
             self.returncode = self.po.poll()
-            if raise_except:
-                raise
+            logger.debug("Popen returncode: %d, poll() returncode: %d", self.po.returncode, self.returncode)
         return (_stdout, _stderr)
 
     def __getattr__(self, name):
         return getattr(self.po, name)
+
+
+class Tee(object):
+    def __init__(self):
+        self.writers = []
+        # Wrap sys.stdout into a StreamWriter to allow writing unicode.
+        # https://stackoverflow.com/a/4546129/542082
+        #  if not sys.stdout.isatty():
+        #      sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
+        self._preferred_encoding = getpreferredencoding()
+
+    def __del__(self):
+        self.close()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self.close()
+
+    def attach_file(self, writer):
+        def flush(stream):
+            stream.flush()
+            os.fsync(stream.fileno())
+
+        _decoder = (lambda msg: msg.decode(writer.encoding)) if writer.encoding else (lambda msg: msg)
+        self.writers.append((writer, _decoder, flush, lambda stream: stream.close()))
+
+    def attach_std(self, writer=sys.stdout):
+        def flush(stream):
+            stream.flush()
+
+        _decoder = (lambda msg: msg.decode(writer.encoding)) if writer.encoding else (lambda msg: msg)
+        self.writers.append((writer, _decoder, flush, lambda _: ()))
+
+    def write(self, message):
+        for writer, decoder, _, _ in self.writers:
+            if not writer.closed:
+                writer.write(decoder(message))
+
+    def flush(self):
+        for stream, _, flusher, _ in self.writers:
+            flusher(stream)
+
+    def close(self):
+        while self.writers:
+            stream, _, _, closer = self.writers.pop()
+            closer(stream)
+
+
+#https://stackoverflow.com/a/54868710/542082
+class ProcessRunner(object):
+    def __init__(self, args, timeout=None, bufsize=-1, seconds_to_wait=0.25, **kwargs):
+        """Constructor facade to subprocess.Popen that receives parameters
+        which are more specifically required for the.
+
+        Process Runner. This is a class that should be used as a context manager - and that provides an iterator
+        for reading captured output from subprocess.communicate in near realtime.
+
+        Example usage:
+
+        exitcode = -9
+        with Tee() as tee:
+            tee.attach_std()
+            process_runner = None
+            try:
+                with ProcessRunner(('ls', '-lAR', '.'), seconds_to_wait=0.25) as process_runner:
+                    for out, err in process_runner:
+                        tee.write(out)
+                    exitcode = process_runner.returncode
+            except CalledProcessError as e:
+                logger.debug("non-zero exitcode: %s", e)
+            except TimeoutExpired as e:
+                logger.debug(e)
+            except OSError as e:
+                logger.debug("executable error: %s", e)
+                # follow shell exit code
+                exitcode = 127
+            except Exception as e:
+                logger.debug("process creation failure: %s", e)
+            finally:
+                if process_runner:
+                    exitcode = process_runner.returncode
+
+        :param args: same as subprocess.Popen
+        :param timeout: same as subprocess.communicate
+        :param bufsize: same as subprocess.Popen
+        :param seconds_to_wait: time to wait between each readline from the temporary file
+        :param kwargs: same as subprocess.Popen
+        """
+        self._seconds_to_wait = seconds_to_wait
+        self._process_has_timed_out = False
+        self._timeout = timeout
+        self._has_timeout = has_timeout_param
+        self._process_done = False
+        self._stdout_file_handle = NamedTemporaryFile()
+        self._stderr_file_handle = kwargs.pop('stderr', None)
+        # DEVNULL and STDOUT will be passed through
+        # redirect to err file otherwise
+        if self._stderr_file_handle in (None, PIPE):
+            self._stderr_file_handle = NamedTemporaryFile()
+        self._stdin_handle = kwargs.pop('stdin_handle', None)
+        if not self._stdin_handle is None:
+            kwargs.setdefault('stdin', PIPE)
+
+        _exec_using_shell = kwargs.get('shell', False)
+        self._command_list = args
+        if not _exec_using_shell and not isinstance(args, list) and not isinstance(args, tuple):
+            self._command_list = shlex.split(args)
+        logger.debug("Executing command: %s with kwargs: %s", self._command_list, kwargs)
+
+        self._process = Popen(self._command_list,
+                              bufsize=bufsize,
+                              stdout=self._stdout_file_handle,
+                              stderr=self._stderr_file_handle,
+                              **kwargs)
+        self._thread = Thread(target=self._run_process)
+        self._thread.daemon = True
+
+    def __enter__(self):
+        self._thread.start()
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self._thread.join()
+        self._stdout_file_handle.close()
+        # for newer versions, DEVNULL should also be masked
+        if not self._stderr_file_handle == STDOUT:
+            self._stderr_file_handle.close()
+
+    def __iter__(self):
+        # read all output from stdout file that subprocess.communicate fills
+        _firstiter = True
+        _stderr = None
+        _err = ''
+        try:
+            if not self._stderr_file_handle == STDOUT:
+                _stderr = open(self._stderr_file_handle.name, 'r')
+            with open(self._stdout_file_handle.name, 'r') as _stdout:
+                # while process is alive, keep reading data
+                while not self._process_done:
+                    _out = _stdout.readline()
+                    if _stderr:
+                        _err = _stderr.readline()
+                    if _out or _err:
+                        yield (_out, _err)
+                    else:
+                        # if there is nothing to read, then please wait a tiny little bit
+                        if _firstiter:
+                            time.sleep(0.001)
+                            _firstiter = False
+                            continue
+                        logger.debug("Command %s (pid: %s) has nothing to read from, sleeping for %ss",
+                                     self._command_list, self._process.pid, self._seconds_to_wait)
+                        time.sleep(self._seconds_to_wait)
+
+                # catch writes to buffer after process has finished
+                _out = _stdout.read()
+                if _stderr:
+                    _err = _stderr.read()
+                if _out or _err:
+                    yield (_out, _err)
+        finally:
+            if _stderr:
+                _stderr.close()
+
+        if self._process_has_timed_out:
+            raise TimeoutExpired(self._command_list, self._timeout)
+
+        if self._process.returncode != 0:
+            raise CalledProcessError(self.returncode, self._command_list)
+
+    def _run_process(self):
+        try:
+            # Start gathering information (stdout and stderr) from the opened process
+            self._process.communicate(input=self._stdin_handle, timeout=self._timeout)
+            # Graceful termination of the opened process
+            self._process.terminate()
+        except TimeoutExpired:
+            # Force termination of the opened process
+            self._process.terminate()
+            self._process.communicate(timeout=5.0)
+            self._process_has_timed_out = True
+
+        self._process_done = True
+
+    @property
+    def returncode(self):
+        return self._process.returncode
diff --git a/SConsider/SomeUtils.py b/SConsider/SomeUtils.py
index dd93d45bde964062ddb500fda4c814c4264f07cc..b421fbdab71dfb548035f338f29682f5b6c0e5da 100644
--- a/SConsider/SomeUtils.py
+++ b/SConsider/SomeUtils.py
@@ -15,7 +15,9 @@ Collection of helper functions
 
 import os
 import re
-from SConsider.PopenHelper import PopenHelper, PIPE
+from logging import getLogger
+from SConsider.PopenHelper import ProcessRunner, CalledProcessError, TimeoutExpired
+logger = getLogger(__name__)
 
 
 def FileNodeComparer(left, right):
@@ -325,8 +327,7 @@ def getfqdn():
     return (hostname, domain, fqdn)
 
 
-def runCommand(args, logpath='', filename=None, stdincontent=None, timeout=120, **kw):
-    res = 1
+def runCommand(args, logpath='', filename=None, stdincontent=None, timeout=120.0, **kw):
     if filename:
         with open(filename) as f:
             stdincontent = f.read()
@@ -341,8 +342,6 @@ def runCommand(args, logpath='', filename=None, stdincontent=None, timeout=120,
     if callable(filter_func):
         stdincontent = filter_func(stdincontent)
 
-    popenObject = PopenHelper(args, stdin=PIPE, stderr=PIPE, stdout=PIPE, **kw)
-
     if not os.path.isdir(logpath):
         os.makedirs(logpath)
     logfilebasename = os.path.basename(args[0])
@@ -350,21 +349,33 @@ def runCommand(args, logpath='', filename=None, stdincontent=None, timeout=120,
         logfilebasename = logfilebasename + '.' + os.path.basename(filename)
     errfilename = os.path.join(logpath, logfilebasename + '.stderr')
     outfilename = os.path.join(logpath, logfilebasename + '.stdout')
+
+    executor = None
     try:
-        popen_out, popen_err = popenObject.communicate(stdincontent, timeout=timeout)
-        if popen_err:
-            with open(errfilename, 'w') as errfile:
-                errfile.write(popen_err)
-        if popen_out:
-            with open(outfilename, 'w') as outfile:
-                outfile.write(popen_out)
-        res = popenObject.returncode
-    except OSError as ex:
-        with open(errfilename, 'w') as errfile:
-            print >> errfile, ex
-            for line in popenObject.stderr:
-                print >> errfile, line
-    return res
+        errfile = open(errfilename, 'w')
+        outfile = open(outfilename, 'w')
+        kw.setdefault('seconds_to_wait', 0.1)
+        with ProcessRunner(args, timeout=timeout, stdin_handle=stdincontent, **kw) as executor:
+            for out, err in executor:
+                outfile.write(out)
+                errfile.write(err)
+    except CalledProcessError as e:
+        logger.debug("non-zero exitcode: %s", e)
+    except TimeoutExpired as e:
+        logger.debug(e)
+    except OSError as e:
+        logger.debug("executable error: %s", e)
+        # follow shell exit code
+        exitcode = 127
+    except Exception as e:
+        logger.debug("process creation failure: %s", e)
+        print >> errfile, e
+    finally:
+        if executor:
+            exitcode = executor.returncode
+        errfile.close()
+        outfile.close()
+    return exitcode
 
 
 def getLibCVersion(bits='32'):
diff --git a/SConsider/site_tools/DoxygenBuilder.py b/SConsider/site_tools/DoxygenBuilder.py
index 9e93308fc907341766f2856278d436ecdfb02eb4..8ea122b1e91eca40713430562587e9a2293fd387 100644
--- a/SConsider/site_tools/DoxygenBuilder.py
+++ b/SConsider/site_tools/DoxygenBuilder.py
@@ -18,7 +18,7 @@ from __future__ import with_statement
 import os
 import re
 from logging import getLogger
-from SConsider.PopenHelper import PopenHelper, PIPE
+from SConsider.PopenHelper import ProcessRunner
 logger = getLogger(__name__)
 
 
@@ -78,12 +78,13 @@ def getPackageInputDirs(registry, packagename, relativeTo=None):
         else:
             return abspath
 
+    import SCons
     for _, settings in buildSettings.items():
-        import SCons
         # directories of own cpp files
         for sourcefile in settings.get('sourceFiles', []):
-            if isinstance(sourcefile, SCons.Node.FS.File):
-                sourceDirs.add(resolvePath(sourcefile.srcnode().dir.get_abspath(), relativeTo))
+            if not isinstance(sourcefile, SCons.Node.FS.File):
+                sourcefile = includeBasedir.File(sourcefile)
+            sourceDirs.add(resolvePath(sourcefile.srcnode().dir.get_abspath(), relativeTo))
 
         # include directory of own private headers
         includeSubdirPrivate = settings.get('includeSubdir', '')
@@ -95,8 +96,9 @@ def getPackageInputDirs(registry, packagename, relativeTo=None):
 
         # directories of own public headers which are going to be copied
         for sourcefile in settings.get('public', {}).get('includes', []):
-            if isinstance(sourcefile, SCons.Node.FS.File):
-                sourceDirs.add(resolvePath(sourcefile.srcnode().dir.get_abspath(), relativeTo))
+            if not isinstance(sourcefile, SCons.Node.FS.File):
+                sourcefile = includeBasedir.File(sourcefile)
+            sourceDirs.add(resolvePath(sourcefile.srcnode().dir.get_abspath(), relativeTo))
 
     return sourceDirs
 
@@ -143,9 +145,12 @@ def setDoxyfileData(doxyfile, doxyDict):
 
 
 def getDoxyfileTemplate():
-    proc = PopenHelper(['doxygen', '-s', '-g', '-'], stdout=PIPE, stderr=PIPE)
-    stdout, _ = proc.communicate()
-    return parseDoxyfileContent(stdout, {})
+    _cmd = ['doxygen', '-s', '-g', '-']
+    _out = ''
+    with ProcessRunner(_cmd, timeout=30) as executor:
+        for out, _ in executor:
+            _out += out
+    return parseDoxyfileContent(_out, {})
 
 
 def parseDoxyfile(file_node, env):
@@ -326,10 +331,13 @@ def buildDoxyfile(target, env, **kw):
                 doxyfile.write('%s \\\n' % define)
         doxyfile.write('\n')
 
-    log_out, log_err = openLogFiles(env)
     try:
-        proc = PopenHelper(['doxygen', '-s', '-u', target[0].get_abspath()], stdout=log_out, stderr=log_err)
-        proc.wait()
+        log_out, log_err = openLogFiles(env)
+        _cmd = ['doxygen', '-s', '-u', target[0].get_abspath()]
+        with ProcessRunner(_cmd, timeout=60) as executor:
+            for out, err in executor:
+                log_out.write(out)
+                log_err.write(err)
     finally:
         closeLogFiles(log_out, log_err)
 
@@ -373,10 +381,12 @@ def callDoxygen(source, env, **kw):
 
     cmd = 'cd %s && %s %s' % (source[0].get_dir().get_abspath(), 'doxygen', os.path.basename(str(source[0])))
 
-    log_out, log_err = openLogFiles(env)
     try:
-        proc = PopenHelper(cmd, shell=True, stdout=log_out, stderr=log_err)
-        proc.wait()
+        log_out, log_err = openLogFiles(env)
+        with ProcessRunner(cmd, timeout=300, shell=True) as executor:
+            for out, err in executor:
+                log_out.write(out)
+                log_err.write(err)
     finally:
         closeLogFiles(log_out, log_err)
 
@@ -654,13 +664,6 @@ class DoxygenToolException(Exception):
 
 
 def determineCompilerDefines(env):
-    cmd = 'PATH=' + env.get('ENV', []).get('PATH', '')
-    cmd += ' ' + os.path.join(os.path.dirname(__file__), 'defines.sh')
-    cmd += ' ' + env['CXX']
-
-    proc = PopenHelper(cmd, shell=True, stdout=PIPE, stderr=PIPE)
-    stdoutdata, _ = proc.communicate()
-
     defines = {}
     ignores = [
         'cc',
@@ -672,10 +675,17 @@ def determineCompilerDefines(env):
         '__TIMESTAMP__',
     ]
     pattern = re.compile(r'^([^\s]+)\s*=\s*(.*)\s*')
-    for line in stdoutdata.splitlines():
-        match = re.match(pattern, line)
-        if match and match.group(1) not in ignores:
-            defines[match.group(1)] = match.group(2)
+
+    cmd = 'PATH=' + env.get('ENV', []).get('PATH', '')
+    cmd += ' ' + os.path.join(os.path.dirname(__file__), 'defines.sh')
+    cmd += ' ' + env['CXX']
+
+    with ProcessRunner(cmd, timeout=60, shell=True) as executor:
+        for out, _ in executor:
+            for line in out.splitlines():
+                match = re.match(pattern, line)
+                if match and match.group(1) not in ignores:
+                    defines[match.group(1)] = match.group(2)
 
     return defines
 
diff --git a/SConsider/site_tools/RunBuilder.py b/SConsider/site_tools/RunBuilder.py
index b835c87d9849155f17dab4e648d57f32bd0029a4..59e1ebc55e0ef91c6cd60717a804228e910c2681 100644
--- a/SConsider/site_tools/RunBuilder.py
+++ b/SConsider/site_tools/RunBuilder.py
@@ -20,9 +20,9 @@ setup/teardown functions executed before and after running the program.
 from __future__ import with_statement
 import os
 import optparse
-import sys
 import shlex
 from logging import getLogger
+import sys
 from SCons.Action import Action
 from SCons.Builder import Builder
 from SCons.Script import AddOption, GetOption, COMMAND_LINE_TARGETS
@@ -30,10 +30,11 @@ from SCons.Util import is_List
 from SConsider.PackageRegistry import PackageRegistry
 from SConsider.Callback import Callback
 from SConsider.SomeUtils import hasPathPart, isFileNode, isDerivedNode, getNodeDependencies, getFlatENV
-from SConsider.PopenHelper import PopenHelper, PIPE, STDOUT
+from SConsider.PopenHelper import ProcessRunner, Tee, CalledProcessError, TimeoutExpired, STDOUT
 logger = getLogger(__name__)
 
 runtargets = {}
+_DEFAULT_TIMEOUT = 120.0
 
 
 def setTarget(packagename, targetname, target):
@@ -57,52 +58,38 @@ def getTargets(packagename=None, targetname=None):
     return targets
 
 
-class Tee(object):
-    def __init__(self):
-        self.writers = []
-
-    def add(self, writer, flush=False, close=True):
-        self.writers.append((writer, flush, close))
-
-    def write(self, output):
-        for writer, flush, _ in self.writers:
-            writer.write(output)
-            if flush:
-                writer.flush()
-
-    def close(self):
-        for writer, _, close in self.writers:
-            if close:
-                writer.close()
-
-
 def run(cmd, logfile=None, **kw):
     """Run a Unix command and return the exit code."""
-    tee = Tee()
-    tee.add(sys.stdout, flush=True, close=False)
-    rcode = 99
-    proc = None
-    try:
+    exitcode = 99
+    with Tee() as tee:
+        tee.attach_std()
         if logfile:
             if not os.path.isdir(logfile.dir.get_abspath()):
                 os.makedirs(logfile.dir.get_abspath())
-            tee.add(open(logfile.get_abspath(), 'w'))
-        proc = PopenHelper(cmd, stdin=None, stdout=PIPE, stderr=STDOUT, **kw)
-        while True:
-            out = proc.stdout.read(1)
-            if out == '' and proc.poll() is not None:
-                break
-            tee.write(out)
-        rcode = proc.returncode
-    finally:
-        while True and proc:
-            out = proc.stdout.readline()
-            if out == '' and proc.poll() is not None:
-                break
-            tee.write(out)
-        tee.close()
-
-    return rcode
+            tee.attach_file(open(logfile.get_abspath(), 'w'))
+        process_runner = None
+        try:
+            #FIXME: add timeout parameter
+            with ProcessRunner(cmd, stderr=STDOUT, seconds_to_wait=0.2, **kw) as process_runner:
+                for out, _ in process_runner:
+                    tee.write(out)
+                exitcode = process_runner.returncode
+        except CalledProcessError as e:
+            logger.debug("non-zero exitcode: %s", e)
+        except TimeoutExpired as e:
+            logger.debug(e)
+        except OSError as e:
+            logger.debug("executable error: %s", e)
+            # follow shell exit code
+            exitcode = 127
+        except Exception as e:
+            logger.debug("process creation failure: %s", e)
+        finally:
+            if process_runner:
+                exitcode = process_runner.returncode
+
+    logger.debug("returncode: %d", exitcode)
+    return exitcode
 
 
 def emitPassedFile(target, source, env):
@@ -120,7 +107,10 @@ def execute(command, env):
     if 'mingw' in env["TOOLS"]:
         args.insert(0, "sh.exe")
 
-    return run(args, env=getFlatENV(env), logfile=env.get('logfile', None))
+    return run(args,
+               env=getFlatENV(env),
+               logfile=env.get('logfile', None),
+               timeout=env.get('timeout', _DEFAULT_TIMEOUT))
 
 
 def doTest(target, source, env):
@@ -144,15 +134,28 @@ def doRun(target, source, env):
     return res
 
 
-def getRunParams(buildSettings, defaultRunParams):
+def getRunParams(buildSettings, default):
+    runConfig = buildSettings.get('runConfig', {})
+    params = GetOption('runParams')
+    if not params:
+        if not runConfig:
+            runConfig = dict()
+        params = runConfig.get('runParams', default)
+    if isinstance(params, list):
+        params = ' '.join(params)
+    return params
+
+
+def getRunTimeout(buildSettings, default):
     runConfig = buildSettings.get('runConfig', {})
-    if GetOption('runParams'):
-        runParams = " ".join(GetOption('runParams'))
-    else:
+    param = GetOption('runTimeout')
+    if param < 0.0:
         if not runConfig:
             runConfig = dict()
-        runParams = runConfig.get('runParams', defaultRunParams)
-    return runParams
+        param = runConfig.get('runTimeout', default)
+    if param <= 0.0:
+        param = None
+    return param
 
 
 class SkipTest(Exception):
@@ -204,7 +207,11 @@ def createTestTarget(env, source, packagename, targetname, settings, defaultRunP
         return (source, fullTargetName)
 
     logfile = env.getLogInstallDir().File(targetname + '.test.log')
-    runner = env.TestBuilder([], source, runParams=getRunParams(settings, defaultRunParams), logfile=logfile)
+    runner = env.TestBuilder([],
+                             source,
+                             runParams=getRunParams(settings, defaultRunParams),
+                             logfile=logfile,
+                             timeout=getRunTimeout(settings, default=_DEFAULT_TIMEOUT))
     if GetOption('run-force'):
         env.AlwaysBuild(runner)
 
@@ -251,7 +258,8 @@ def createRunTarget(env, source, packagename, targetname, settings, defaultRunPa
     runner = env.RunBuilder(['dummyRunner_' + fullTargetName],
                             source,
                             runParams=getRunParams(settings, defaultRunParams),
-                            logfile=logfile)
+                            logfile=logfile,
+                            timeout=getRunTimeout(settings, default=_DEFAULT_TIMEOUT))
 
     addRunConfigHooks(env, source, runner, settings)
 
@@ -281,18 +289,27 @@ def composeRunTargets(env, source, packagename, targetname, settings, defaultRun
 
 def generate(env):
     try:
-        AddOption('--run', dest='run', action='store_true', default=False, help='Should we run the target')
+        AddOption('--run',
+                  dest='run',
+                  action='store_true',
+                  default=False,
+                  help='Run the target if not done yet')
         AddOption('--run-force',
                   dest='run-force',
                   action='store_true',
                   default=False,
-                  help='Should we run the target and ignore .passed files')
+                  help='Run the target regardless of the last state (.passed file)')
         AddOption('--runparams',
                   dest='runParams',
                   action='append',
                   type='string',
                   default=[],
                   help='The parameters to hand over')
+        AddOption('--run-timeout',
+                  dest='runTimeout',
+                  action='store',
+                  type='float',
+                  help='Time in seconds after which the running process gets killed')
     except optparse.OptionConflictError:
         pass
 
diff --git a/SConsider/site_tools/TestfwTransformer.py b/SConsider/site_tools/TestfwTransformer.py
index 1385c292b179415c3a0f8c0b669830d7762f5064..9a153a7efd139b38b55c6512c1cc312200defc07 100644
--- a/SConsider/site_tools/TestfwTransformer.py
+++ b/SConsider/site_tools/TestfwTransformer.py
@@ -253,6 +253,7 @@ def callPostTest(target, packagename, targetname, logfile, **kw):
             with open(logfile.get_abspath()) as f:
                 result = parser.parse(f)
                 with open(logfile.dir.File(targetname + '.test.xml').get_abspath(), 'w') as xmlfile:
+                    xmlfile.write('<?xml version="1.0" encoding="UTF-8" ?>')
                     xmlfile.write(result.toXML(packagename + '.' + targetname))
 
 
diff --git a/SConsider/site_tools/g++.py b/SConsider/site_tools/g++.py
index 7abc5a84965c539cf1e1c87d3b71d0af64737ee2..1db68da46e11d68c68b3eb9f9cf9ece9fc328478 100644
--- a/SConsider/site_tools/g++.py
+++ b/SConsider/site_tools/g++.py
@@ -18,7 +18,7 @@ import re
 from logging import getLogger
 import SCons.Tool
 import SCons.Util
-from SConsider.PopenHelper import PopenHelper, PIPE
+from SConsider.PopenHelper import ProcessRunner
 logger = getLogger(__name__)
 
 compilers = ['g++']
@@ -57,10 +57,15 @@ def generate(env):
 
     bitwidth = env.getBitwidth()
     if compiler_subject:
-        _proc = PopenHelper([compiler_subject, '--version'], stdout=PIPE, stderr=PIPE)
-        _out, _err = _proc.communicate()
-
-        if _proc.returncode != 0:
+        _cmd = [compiler_subject, '--version']
+        _out = ''
+        _err = ''
+        with ProcessRunner(_cmd, timeout=20, seconds_to_wait=0.1) as executor:
+            for out, err in executor:
+                _out += out
+                _err += err
+
+        if executor.returncode != 0:
             return
         # -dumpversion was added in GCC 3.0.  As long as we're supporting
         # GCC versions older than that, we should use --version and a
@@ -90,10 +95,13 @@ def generate(env):
             logger.error("failed to create compiler input file, check folder permissions and retry",
                          exc_info=True)
             return
-        _proc = PopenHelper([compiler_subject, '-v', '-xc++', tFile, '-o', outFile, '-m' + bitwidth],
-                            stdout=PIPE,
-                            stderr=PIPE)
-        _out, _err = _proc.communicate()
+        _cmd = [compiler_subject, '-v', '-xc++', tFile, '-o', outFile, '-m' + bitwidth]
+        _out = ''
+        _err = ''
+        with ProcessRunner(_cmd, timeout=20, seconds_to_wait=0.1) as executor:
+            for out, err in executor:
+                _out += out
+                _err += err
 
         text_to_join = ['---- stdout ----', _out, '---- stderr ----', _err]
         build_output = os.linesep.join(text_to_join)
@@ -109,7 +117,7 @@ def generate(env):
                          exc_info=True)
             raise SCons.Errors.UserError(
                 'Build aborted, {0} compiler detection failed!'.format(compiler_subject))
-        if _proc.returncode != 0:
+        if executor.returncode != 0:
             logger.error("compile command failed with return code {0}:".format(proc.returncode) + os.linesep +
                          build_output)
             raise SCons.Errors.UserError(
diff --git a/SConsider/site_tools/gcc.py b/SConsider/site_tools/gcc.py
index 4857c9efcf3bfbdbfb7e0357d49570f1d8ccfbab..b2c06766ff5f2c32cfd91c5350ae626595d5b0b9 100644
--- a/SConsider/site_tools/gcc.py
+++ b/SConsider/site_tools/gcc.py
@@ -18,7 +18,7 @@ import re
 from logging import getLogger
 import SCons.Tool
 import SCons.Util
-from SConsider.PopenHelper import PopenHelper, PIPE
+from SConsider.PopenHelper import ProcessRunner
 logger = getLogger(__name__)
 
 compilers = ['gcc', 'cc']
@@ -44,10 +44,15 @@ def generate(env):
 
     bitwidth = env.getBitwidth()
     if compiler_subject:
-        _proc = PopenHelper([compiler_subject, '--version'], stdout=PIPE, stderr=PIPE)
-        _out, _err = _proc.communicate()
-
-        if _proc.returncode != 0:
+        _cmd = [compiler_subject, '--version']
+        _out = ''
+        _err = ''
+        with ProcessRunner(_cmd, timeout=20, seconds_to_wait=0.1) as executor:
+            for out, err in executor:
+                _out += out
+                _err += err
+
+        if executor.returncode != 0:
             return
         # -dumpversion was added in GCC 3.0.  As long as we're supporting
         # GCC versions older than that, we should use --version and a
@@ -76,10 +81,13 @@ def generate(env):
             logger.error("failed to create compiler input file, check folder permissions and retry",
                          exc_info=True)
             return
-        _proc = PopenHelper([compiler_subject, '-v', '-xc', tFile, '-o', outFile, '-m' + bitwidth],
-                            stdout=PIPE,
-                            stderr=PIPE)
-        _out, _err = _proc.communicate()
+        _cmd = [compiler_subject, '-v', '-xc', tFile, '-o', outFile, '-m' + bitwidth]
+        _out = ''
+        _err = ''
+        with ProcessRunner(_cmd, timeout=20, seconds_to_wait=0.1) as executor:
+            for out, err in executor:
+                _out += out
+                _err += err
 
         text_to_join = ['---- stdout ----', _out, '---- stderr ----', _err]
         build_output = os.linesep.join(text_to_join)
@@ -95,7 +103,7 @@ def generate(env):
                          exc_info=True)
             raise SCons.Errors.UserError(
                 'Build aborted, {0} compiler detection failed!'.format(compiler_subject))
-        if _proc.returncode != 0:
+        if executor.returncode != 0:
             logger.error("compile command failed with return code {0}:".format(proc.returncode) + os.linesep +
                          build_output)
             raise SCons.Errors.UserError(
diff --git a/SConsider/site_tools/generateScript.py b/SConsider/site_tools/generateScript.py
index b535f972e2f46dea96b0ebb9f6414f22b85c7e59..064af53a1fef5e0ead1292a231902fa784f99575 100644
--- a/SConsider/site_tools/generateScript.py
+++ b/SConsider/site_tools/generateScript.py
@@ -266,17 +266,15 @@ if [ ${doDebug:-0} -ge 1 ]; then
     test ${doTrace} -eq 1 && cat ${cfg_gdbcommands}
     cfg_gdbcommands="--command $cfg_gdbcommands";
     test $doDebug -gt 1 && cfg_gdbcommands="--batch $cfg_gdbcommands";
-    eval gdb ${cfg_gdbcommands}
+    exec eval gdb ${cfg_gdbcommands}
 elif [ ${doDebugServer:-0} -eq 1 -a -x "$(type -fP gdbserver 2>/dev/null)" ]; then
-    gdbserver :${GDBSERVERPORT} "${CMD}" "$@"
+    exec gdbserver :${GDBSERVERPORT} "${CMD}" "$@"
 elif [ ${doCommandWithArgs:-0} -eq 1 ]; then
     test ${doTrace} -eq 1 && echo "executing command [${cmdArr[*]} ${CMD} $@]"
-    eval "${cmdArr[*]} ${CMD} $@"
+    exec eval "${cmdArr[*]} ${CMD} $@"
 else
-    "$CMD" "$@"
+    exec "$CMD" "$@"
 fi
-
-exit $?
 """
     scriptFile.write(scriptText)
 
diff --git a/VERSION b/VERSION
index b88c14da673b43c3ea634c60b8f87dd47d87c13b..6e46293aeb25f76497c29f5f8c386c931b5408b8 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.3.16
\ No newline at end of file
+0.3.17
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index d62a7b08175e82b95497979e695a81ad7c750049..c5fe694928d4c058e47df8ca333bd57423e91654 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -67,3 +67,19 @@ def copy_testdir_to_tmp(tmpdir_factory, invocation_path, current_testdir):
     sources_path = invocation_path(current_testdir)
     shutil.copytree(str(sources_path), str(fn), ignore=ignorefiles)
     return fn
+
+
+# https://stackoverflow.com/a/51495108/542082
+@pytest.fixture(scope='function', autouse=True)
+def testcase_result(request):
+    def fin():
+        print(" (DURATION={})".format(request.node.rep_call.duration))
+
+    request.addfinalizer(fin)
+
+
+@pytest.mark.tryfirst
+def pytest_runtest_makereport(item, call, __multicall__):
+    rep = __multicall__.execute()
+    setattr(item, "rep_" + rep.when, rep)
+    return rep
diff --git a/tests/invocation/debuglog.yaml b/tests/invocation/debuglog.yaml
index 239ed680fd92b8a12203d03f99184c937f52557a..36e97ee2fa58af4fa23322ca5979bf432feafb2c 100644
--- a/tests/invocation/debuglog.yaml
+++ b/tests/invocation/debuglog.yaml
@@ -21,6 +21,10 @@ filters:
         '()': SConsider.Logging.RegexFilter
         pattern: "^executing \\["
         flags: 0
+    filterProcessHelperWaitOnStream:
+        '()': SConsider.Logging.RegexFilter
+        pattern: ".*has nothing to read from, sleeping for.*"
+        flags: 0
 
 handlers:
     console:
@@ -54,6 +58,23 @@ loggers:
         propagate: no
         filters: []
 
+    g++:
+        level: INFO
+        handlers: [console]
+        propagate: no
+        filters: []
+
+    gcc:
+        level: INFO
+        handlers: [console]
+        propagate: no
+        filters: []
+
+    py.warnings:
+        level: WARNING
+        handlers: [console]
+        propagate: no
+
     TargetMaker:
         level: DEBUG
         handlers: [console]
@@ -72,6 +93,12 @@ loggers:
         propagate: no
         filters: [filterLookup]
 
+    SConsider.PopenHelper:
+        level: DEBUG
+        handlers: [console]
+        propagate: no
+        filters: [filterProcessHelperWaitOnStream]
+
     setupBuildTools:
         level: DEBUG
         handlers: [console]
diff --git a/tests/invocation/infolog.yaml b/tests/invocation/infolog.yaml
index 8b82c591053f85d84651e63a734d25f21f9a510c..df2d484e4d75f12fe63c7e09e9aa8dc4f49889dc 100644
--- a/tests/invocation/infolog.yaml
+++ b/tests/invocation/infolog.yaml
@@ -21,6 +21,10 @@ filters:
         '()': SConsider.Logging.RegexFilter
         pattern: "^executing \\["
         flags: 0
+    filterProcessHelperWaitOnStream:
+        '()': SConsider.Logging.RegexFilter
+        pattern: ".*has nothing to read from, sleeping for.*"
+        flags: 0
 
 handlers:
     console:
@@ -54,6 +58,23 @@ loggers:
         propagate: no
         filters: []
 
+    g++:
+        level: INFO
+        handlers: [console]
+        propagate: no
+        filters: []
+
+    gcc:
+        level: INFO
+        handlers: [console]
+        propagate: no
+        filters: []
+
+    py.warnings:
+        level: WARNING
+        handlers: [console]
+        propagate: no
+
     TargetMaker:
         level: WARNING
         handlers: [console]
@@ -72,6 +93,12 @@ loggers:
         propagate: no
         filters: [filterLookup, filterExecuting]
 
+    SConsider.PopenHelper:
+        level: INFO
+        handlers: [console]
+        propagate: no
+        filters: []
+
     setupBuildTools:
         level: INFO
         handlers: [console]
@@ -90,4 +117,5 @@ loggers:
 root:
     level: INFO
     handlers: [console, info_file_handler, error_file_handler]
+    filters: []
 ...
diff --git a/tests/test_ProcessRunner.py b/tests/test_ProcessRunner.py
new file mode 100644
index 0000000000000000000000000000000000000000..a9cf1362d1a7384d0134192d7a225a92dd04d336
--- /dev/null
+++ b/tests/test_ProcessRunner.py
@@ -0,0 +1,93 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2016, Peter Sommerlad and IFS Institute for Software
+# at HSR Rapperswil, Switzerland
+# All rights reserved.
+#
+# This library/application is free software; you can redistribute and/or
+# modify it under the terms of the license that is included with this
+# library/application in the file license.txt.
+# -------------------------------------------------------------------------
+
+import pytest
+import sys
+from SConsider.PopenHelper import ProcessRunner, STDOUT, CalledProcessError, TimeoutExpired
+
+
+def test_CommandStringWithStdout(capfd):
+    cmd = 'ls'
+    with ProcessRunner(cmd) as executor:
+        for out, _ in executor:
+            sys.stdout.write(out)
+    assert 0 == executor.returncode
+    captured = capfd.readouterr()
+    assert 'setup.py' in captured.out
+
+
+def test_CommandStringWithStderr(capfd):
+    with pytest.raises(CalledProcessError) as excinfo:
+        cmd = 'ls _NotEx.isting_'
+        with ProcessRunner(cmd) as executor:
+            for out, err in executor:
+                sys.stdout.write(out)
+                sys.stderr.write(err)
+    assert 2 == excinfo.value.returncode
+    captured = capfd.readouterr()
+    assert 'cannot access' in captured.err
+    assert '' == captured.out
+
+
+def test_CommandStringWithStderrOnStdout(capfd):
+    with pytest.raises(CalledProcessError) as excinfo:
+        cmd = 'ls _NotEx.isting_'
+        with ProcessRunner(cmd, stderr=STDOUT) as executor:
+            for out, err in executor:
+                sys.stdout.write(out)
+                sys.stderr.write(err)
+    assert 2 == excinfo.value.returncode
+    captured = capfd.readouterr()
+    assert 'cannot access' in captured.out
+    assert '' == captured.err
+
+
+def test_CommandArrayWithOutput(capfd):
+    cmd = ['ls']
+    with ProcessRunner(cmd) as executor:
+        for out, _ in executor:
+            sys.stdout.write(out)
+    assert 0 == executor.returncode
+    captured = capfd.readouterr()
+    assert 'setup.py' in captured.out
+
+
+def test_CommandStringWithOutputFromInput(capfd):
+    cmd = 'cat -'
+    send_on_stdin = 'loopback string'
+    with ProcessRunner(cmd, timeout=3, stdin_handle=send_on_stdin) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    captured = capfd.readouterr()
+    assert 0 == executor.returncode
+    assert send_on_stdin in captured.out
+    assert '' == captured.err
+
+
+def test_CommandStringWithTimeoutResultsInKill():
+    with pytest.raises(TimeoutExpired) as excinfo:
+        cmd = 'sleep 3'
+        with ProcessRunner(cmd, timeout=1) as executor:
+            for out, _ in executor:
+                sys.stdout.write(out)
+    assert 'sleep' in str(excinfo.value.cmd)
+
+
+def test_CommandStringNotSplitWhenUsingShell(capfd):
+    cmd = r"""for n in 1 2 3; do
+    echo $n;
+done"""
+    with ProcessRunner(cmd, timeout=1, shell=True) as executor:
+        for out, _ in executor:
+            sys.stdout.write(out)
+    assert 0 == executor.returncode
+    captured = capfd.readouterr()
+    assert '1\n2\n3\n' in captured.out
diff --git a/tests/test_SConsInvocations.py b/tests/test_SConsInvocations.py
index c08fe6a5e4ce624da107358a8c6a9797cc8d5f02..c6726c9c8a50337b4b40f9cebdc0a5cd2b9ffab5 100644
--- a/tests/test_SConsInvocations.py
+++ b/tests/test_SConsInvocations.py
@@ -11,8 +11,9 @@
 import pytest
 import os
 import re
+import sys
 from glob import glob
-from SConsider.PopenHelper import PopenHelper
+from SConsider.PopenHelper import ProcessRunner
 
 
 @pytest.mark.invocation
@@ -21,11 +22,13 @@ def test_SConsiderGetHelp(copy_testdir_to_tmp, pypath_extended_env, popen_timeou
                           invocation_path, capfd):
 
     pypath_extended_env.update({'LOG_CFG': str(invocation_path('debuglog.yaml'))})
-    sub_p = PopenHelper(r'scons -h' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons -h' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert '--baseoutdir' in captured.out
     assert '--archbits' in captured.out
@@ -35,11 +38,13 @@ def test_SConsiderGetHelp(copy_testdir_to_tmp, pypath_extended_env, popen_timeou
 @pytest.mark.parametrize('current_testdir', ['gethelp'])
 def test_SConsiderSconstructDirSameAsLaunchDir(copy_testdir_to_tmp, pypath_extended_env, popen_timeout,
                                                scons_platform_options, capfd):
-    sub_p = PopenHelper(r'scons -h' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons -h' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'get_sconstruct_dir=' + str(copy_testdir_to_tmp) in captured.out
     assert 'get_launch_dir=' + str(copy_testdir_to_tmp) in captured.out
@@ -51,9 +56,12 @@ def test_SConsiderSconstructDirSameAsLaunchDir(copy_testdir_to_tmp, pypath_exten
 def test_SConsiderSconstructDirBelowLaunchDir(copy_testdir_to_tmp, pypath_extended_env, popen_timeout,
                                               scons_platform_options, capfd):
     sconstruct_file = os.path.join(str(copy_testdir_to_tmp), 'SConstruct')
-    sub_p = PopenHelper(r'scons -h -f ' + sconstruct_file + scons_platform_options, env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons -h -f ' + sconstruct_file + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'get_sconstruct_dir=' + str(copy_testdir_to_tmp) in captured.out
     assert 'get_launch_dir=' + os.getcwd() in captured.out
@@ -64,11 +72,13 @@ def test_SConsiderSconstructDirBelowLaunchDir(copy_testdir_to_tmp, pypath_extend
 def test_SConsiderSconstructAboveLaunchDir(copy_testdir_to_tmp, pypath_extended_env, popen_timeout,
                                            scons_platform_options, capfd):
     progdir_path = copy_testdir_to_tmp.join('progdir')
-    sub_p = PopenHelper(r'scons -h -u ' + scons_platform_options,
-                        cwd=str(progdir_path),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons -h -u ' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(progdir_path),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'get_sconstruct_dir=' + str(copy_testdir_to_tmp) in captured.out
     assert 'get_launch_dir=' + str(progdir_path) in captured.out
@@ -78,11 +88,13 @@ def test_SConsiderSconstructAboveLaunchDir(copy_testdir_to_tmp, pypath_extended_
 @pytest.mark.invocation
 def test_SConsiderStaticProgBuild(copy_testdir_to_tmp, pypath_extended_env, popen_timeout,
                                   scons_platform_options, capfd):
-    sub_p = PopenHelper(r'scons --3rdparty=' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons --3rdparty=' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'done building targets.' in captured.out
 
@@ -90,11 +102,13 @@ def test_SConsiderStaticProgBuild(copy_testdir_to_tmp, pypath_extended_env, pope
 @pytest.mark.invocation
 def test_SConsiderStaticProgDoxygenOnly(copy_testdir_to_tmp, pypath_extended_env, popen_timeout,
                                         scons_platform_options, capfd):
-    sub_p = PopenHelper(r'scons --usetool=DoxygenBuilder --doxygen-only hello' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons --usetool=DoxygenBuilder --doxygen-only hello' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'done building targets.' in captured.out
     assert re.search('BUILD_TARGETS.*doxygen', captured.out)
@@ -103,11 +117,13 @@ def test_SConsiderStaticProgDoxygenOnly(copy_testdir_to_tmp, pypath_extended_env
 @pytest.mark.invocation
 def test_SConsiderStaticProgIncludingDoxygen(copy_testdir_to_tmp, pypath_extended_env, popen_timeout,
                                              scons_platform_options, capfd):
-    sub_p = PopenHelper(r'scons --usetool=DoxygenBuilder --doxygen hello' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons --usetool=DoxygenBuilder --doxygen hello' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'done building targets.' in captured.out
     assert re.search('BUILD_TARGETS.*doxygen', captured.out)
@@ -117,11 +133,13 @@ def test_SConsiderStaticProgIncludingDoxygen(copy_testdir_to_tmp, pypath_extende
 @pytest.mark.invocation
 def test_SConsiderStaticProgRunWithoutCommandLineTarget(copy_testdir_to_tmp, pypath_extended_env,
                                                         popen_timeout, scons_platform_options, capfd):
-    sub_p = PopenHelper(r'scons --3rdparty= --run' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons --3rdparty= --run' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'Hello from SConsider' in captured.out
 
@@ -129,11 +147,13 @@ def test_SConsiderStaticProgRunWithoutCommandLineTarget(copy_testdir_to_tmp, pyp
 @pytest.mark.invocation
 def test_SConsiderStaticProgRunWithPackageAsTarget(copy_testdir_to_tmp, pypath_extended_env, popen_timeout,
                                                    scons_platform_options, capfd):
-    sub_p = PopenHelper(r'scons --3rdparty= --run hello' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons --3rdparty= --run hello' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'Hello from SConsider' in captured.out
 
@@ -141,11 +161,13 @@ def test_SConsiderStaticProgRunWithPackageAsTarget(copy_testdir_to_tmp, pypath_e
 @pytest.mark.invocation
 def test_SConsiderStaticProgRunWithExplicitPackageTarget(copy_testdir_to_tmp, pypath_extended_env,
                                                          popen_timeout, scons_platform_options, capfd):
-    sub_p = PopenHelper(r'scons --3rdparty= --run hello.runner' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons --3rdparty= --run hello.runner' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'Hello from SConsider' in captured.out
 
@@ -162,11 +184,13 @@ def assert_outputfiles_exist(baseoutdir, predicate=lambda l: l >= 1):
 @pytest.mark.invocation
 def test_SConsiderStaticProgBuildOutputFilesBelowStartdir(copy_testdir_to_tmp, pypath_extended_env,
                                                           popen_timeout, scons_platform_options):
-    sub_p = PopenHelper(r'scons --3rdparty= --run hello.runner' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons --3rdparty= --run hello.runner' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     assert copy_testdir_to_tmp.join('apps').isdir()
     assert copy_testdir_to_tmp.join('progdir').join('.build').isdir()
     assert_outputfiles_exist(str(copy_testdir_to_tmp))
@@ -177,12 +201,13 @@ def test_SConsiderStaticProgBuildOutputFilesInBaseoutdir(copy_testdir_to_tmp, py
                                                          popen_timeout, scons_platform_options,
                                                          tmpdir_factory):
     baseoutdir = str(tmpdir_factory.mktemp('baseoutdir', numbered=True))
-    sub_p = PopenHelper(r'scons --3rdparty= --baseoutdir=' + baseoutdir + ' --run hello.runner' +
-                        scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons --3rdparty= --baseoutdir=' + baseoutdir + ' --run hello.runner' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     assert not copy_testdir_to_tmp.join('apps').isdir()
     assert not copy_testdir_to_tmp.join('progdir').join('.build').isdir()
     assert_outputfiles_exist(str(copy_testdir_to_tmp), lambda l: l == 0)
@@ -198,17 +223,23 @@ def test_SConsiderThirdPartyBuildOnceOnly(copy_testdir_to_tmp, pypath_extended_e
                                           cpp_in_second_build, capfd):
     """Show that using the source directory name as prefix of the 3rdparty
     build output always builds the sources over and over again."""
-    cmdline = r'scons --3rdparty=my3pscons --with-src-3plib=my3psrc' + scons_platform_options
+    cmd = r'scons --3rdparty=my3pscons --with-src-3plib=my3psrc' + scons_platform_options
     if buildprefix:
-        cmdline += ' --3rdparty-build-prefix=' + buildprefix
-    sub_p = PopenHelper(cmdline, cwd=str(copy_testdir_to_tmp), env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+        cmd += ' --3rdparty-build-prefix=' + buildprefix
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert os.sep + '3plib.cpp' in captured.out
-    sub_p = PopenHelper(cmdline, cwd=str(copy_testdir_to_tmp), env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert cpp_in_second_build(os.sep + '3plib.cpp' in captured.out)
 
@@ -217,11 +248,13 @@ def test_SConsiderThirdPartyBuildOnceOnly(copy_testdir_to_tmp, pypath_extended_e
 @pytest.mark.parametrize('current_testdir', ['samedirtest'])
 def test_SConstructAndSConsiderInSameDirBuild(copy_testdir_to_tmp, pypath_extended_env, popen_timeout,
                                               scons_platform_options, capfd):
-    sub_p = PopenHelper(r'scons --3rdparty=' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons --3rdparty=' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'done building targets.' in captured.out
 
@@ -231,10 +264,12 @@ def test_SConstructAndSConsiderInSameDirBuild(copy_testdir_to_tmp, pypath_extend
 def test_SConstructAndSConsiderInSameDirRunWithoutCommandLineTarget(copy_testdir_to_tmp, pypath_extended_env,
                                                                     popen_timeout, scons_platform_options,
                                                                     capfd):
-    sub_p = PopenHelper(r'scons --3rdparty= --run' + scons_platform_options,
-                        cwd=str(copy_testdir_to_tmp),
-                        env=pypath_extended_env)
-    sub_p.communicate(timeout=popen_timeout)
-    assert 0 == sub_p.returncode
+    cmd = r'scons --3rdparty= --run' + scons_platform_options
+    with ProcessRunner(cmd, timeout=popen_timeout, cwd=str(copy_testdir_to_tmp),
+                       env=pypath_extended_env) as executor:
+        for out, err in executor:
+            sys.stdout.write(out)
+            sys.stderr.write(err)
+    assert 0 == executor.returncode
     captured = capfd.readouterr()
     assert 'Hello from SConsider' in captured.out