From 811797e6a74aa18035e2cd6d0d643033c4c19c93 Mon Sep 17 00:00:00 2001
From: "Y. Luis" <yluisrojo@gmail.com>
Date: Fri, 27 Jul 2018 00:23:57 +0100
Subject: [PATCH 01/23] Update .travis.yml

---
 .travis.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.travis.yml b/.travis.yml
index ff9e857..0ec6cd3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -31,7 +31,7 @@ install:
 before_script:
   - git clone https://github.com/SIPp/sipp.git
   - cd sipp
-  - ./build.sh
+  - ./build.sh --full
   - export PATH="$PWD:$PATH"
   - cd ..
 

From 67370d7e0dc5f85cfd8c9cf75eb5b45261e4838c Mon Sep 17 00:00:00 2001
From: Yunior Luis <yluisrojo@gmail.com>
Date: Mon, 20 Aug 2018 11:23:48 +0200
Subject: [PATCH 02/23] Fix sipp build. Test with default Trusty.

---
 .travis.yml | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 0ec6cd3..144d9b6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,4 @@
 language: python
-sudo: false
-dist: precise
 
 cache:
   - bundler
@@ -29,11 +27,13 @@ install:
   - pip list
 
 before_script:
-  - git clone https://github.com/SIPp/sipp.git
-  - cd sipp
-  - ./build.sh --full
+  - wget https://github.com/SIPp/sipp/releases/download/v3.5.2/sipp-3.5.2.tar.gz
+  - tar -xvzf sipp-3.5.2.tar.gz
+  - cd sipp-3.5.2
+  - ./configure
+  - make
   - export PATH="$PWD:$PATH"
   - cd ..
 
 script:
-  - pytest tests/
+  - pytest tests/
\ No newline at end of file

From 4c955a00e7fa165fbdc50538cc8159aed1792782 Mon Sep 17 00:00:00 2001
From: Yunior Luis <yl.rojo@zaleos.net>
Date: Tue, 31 Jul 2018 18:58:42 +0200
Subject: [PATCH 03/23] remote-host as last position argument

Fix sipp build. Test with default Trusty.
---
 pysipp/command.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pysipp/command.py b/pysipp/command.py
index 0000184..417bc6e 100644
--- a/pysipp/command.py
+++ b/pysipp/command.py
@@ -188,8 +188,6 @@ def todict(self):
     # contact info
     '{prefix} ',
     '{bin_path} ',
-    ('{remote_host}', AddrField),  # NOTE: no space
-    ':{remote_port} ',
     ('-i {local_host} ', AddrField),
     '-p {local_port} ',
     '-s {uri_username} ',
@@ -244,6 +242,8 @@ def todict(self):
     ('-trace_logs {trace_log}', BoolField),
     ('-trace_screen {trace_screen}', BoolField),
     ('-error_overwrite {error_overwrite}', BoolField),
+    ('{remote_host}', AddrField),  # NOTE: no space
+    ':{remote_port}',
 ]
 
 

From 06bff09dd457bf3b932d250c9d6830451ef0989f Mon Sep 17 00:00:00 2001
From: Yunior Luis <yl.rojo@zaleos.net>
Date: Tue, 31 Jul 2018 18:58:42 +0200
Subject: [PATCH 04/23] Allow disable screen_file argument.

Added description in README.
---
 README.md       |  9 +++++++++
 pysipp/agent.py | 20 ++++++++++++--------
 2 files changed, 21 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index 96868c6..d1860ad 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,15 @@ scen = pysipp.scenario(dirpath='path/to/test_scenario/',
 scen()
 ```
 
+**pysipp** by default uses `-screen_file` SIPp argument to redirect output,
+ but this argument is only available in SIPp version >= [3.5.0](https://sourceforge.net/p/sipp/mailman/message/34041962/),
+ for lower versions to run properly, this argument must be
+  disable setting `enable_screen_file` to `False`:
+ 
+```python
+scen = pysipp.scenario(enable_screen_file=False)
+```
+
 If you've got multiple such scenario directories you can iterate over
 them:
 
diff --git a/pysipp/agent.py b/pysipp/agent.py
index 1c0881d..a57a312 100644
--- a/pysipp/agent.py
+++ b/pysipp/agent.py
@@ -16,7 +16,6 @@
 
 
 def tuple_property(attrs):
-
     def getter(self):
         tup = tuple(getattr(self, attr) for attr in attrs)
         if all(tup):
@@ -77,10 +76,11 @@ def is_client(self):
     def is_server(self):
         return 'uas' in self.name.lower()
 
-    def iter_logfile_items(self, types_attr='_log_types'):
+    def iter_logfile_items(self, types_attr='_log_types', enable_screen_file=True):
         for name in getattr(self, types_attr):
-            attr_name = name + '_file'
-            yield attr_name, getattr(self, attr_name)
+            if name != 'screen' or enable_screen_file:
+                attr_name = name + '_file'
+                yield attr_name, getattr(self, attr_name)
 
     def iter_toconsole_items(self):
         yield 'screen_file', self.screen_file
@@ -118,11 +118,11 @@ def enable_tracing(self):
             attr_name = 'trace_' + name
             setattr(self, attr_name, True)
 
-    def enable_logging(self, logdir=None, debug=False):
+    def enable_logging(self, logdir=None, debug=False, enable_screen_file=True):
         """Enable agent logging by appending appropriately named log file
         arguments to the underlying command.
         """
-        logattrs = self.iter_logfile_items()
+        logattrs = self.iter_logfile_items(enable_screen_file=enable_screen_file)
         if debug:
             logattrs = itertools.chain(
                 logattrs,
@@ -233,8 +233,9 @@ class ScenarioType(object):
 
     If called it will invoke the standard run hooks.
     """
+
     def __init__(self, agents, defaults, clientdefaults=None,
-                 serverdefaults=None, confpy=None):
+                 serverdefaults=None, confpy=None, enable_screen_file=True):
         # agents iterable in launch-order
         self._agents = agents
         ua_attrs = UserAgent.keys()
@@ -253,6 +254,7 @@ def __init__(self, agents, defaults, clientdefaults=None,
 
         # hook module
         self.mod = confpy
+        self.enable_screen_file = enable_screen_file
 
     @property
     def agents(self):
@@ -330,6 +332,7 @@ def prepare_agent(self, agent):
         """Return a new agent with all default settings applied from this
         scenario
         """
+
         def merge(dicts):
             """Merge dicts without clobbering up to 1 level deep's worth of
             sub-dicts
@@ -364,7 +367,8 @@ def merge(dicts):
         params = merge(ordered)
         log.debug("merged contents:\n{}".format(params))
         ua = UserAgent(defaults=params)
-        ua.enable_logging()
+
+        ua.enable_logging(enable_screen_file=self.enable_screen_file)
 
         # call post defaults hook
         plugin.mng.hook.pysipp_post_ua_defaults(ua=ua)

From ff6ef74e2b8885c018fe41fe7fa7a5eb45970508 Mon Sep 17 00:00:00 2001
From: Konstantinos Tsakiltzidis <ktsakiltzidis@modulus.gr>
Date: Wed, 26 Jun 2019 18:19:04 +0300
Subject: [PATCH 05/23] Add `auth_username` in command sipp_spec

---
 pysipp/command.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pysipp/command.py b/pysipp/command.py
index 417bc6e..e9d4d35 100644
--- a/pysipp/command.py
+++ b/pysipp/command.py
@@ -212,6 +212,7 @@ def todict(self):
     # SIP vars
     '-cid_str {cid_str} ',
     '-base_cseq {base_cseq} ',
+    '-au {auth_username}',
     '-ap {auth_password} ',
     # load settings
     '-r {rate} ',

From 5f6a05dbce669d42dcb56e2b9c7f4c855abfa2c8 Mon Sep 17 00:00:00 2001
From: Konstantinos Tsakiltzidis <ktsakiltzidis@modulus.gr>
Date: Wed, 3 Jul 2019 11:46:21 +0300
Subject: [PATCH 06/23] Fix missing space for `auth_username` in command
 sipp_spec

---
 pysipp/command.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pysipp/command.py b/pysipp/command.py
index e9d4d35..6b87066 100644
--- a/pysipp/command.py
+++ b/pysipp/command.py
@@ -212,7 +212,7 @@ def todict(self):
     # SIP vars
     '-cid_str {cid_str} ',
     '-base_cseq {base_cseq} ',
-    '-au {auth_username}',
+    '-au {auth_username} ',
     '-ap {auth_password} ',
     # load settings
     '-r {rate} ',

From 436431c62b7112f7a3e258a715ff768423a24b48 Mon Sep 17 00:00:00 2001
From: Konstantinos Tsakiltzidis <ktsakiltzidis@modulus.gr>
Date: Wed, 3 Jul 2019 12:04:44 +0300
Subject: [PATCH 07/23] Add Authentication section in README

---
 README.md | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/README.md b/README.md
index d1860ad..20cc728 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,15 @@ uas(block=False)  # returns a `pysipp.launch.PopenRunner` instance by default
 uac()  # run client synchronously
 ```
 
+## Authentication
+When using the `[authentication]` (sipp keyword)[https://sipp.readthedocs.io/en/latest/scenarios/keywords.html#authentication]
+in scenarios, providing the credentials can be done with the
+`auth_username` and `auth_password` arguments, for example:
+
+```python
+pysipp.client(auth_username='sipp', auth_password='sipp-pass')
+```
+
 ## Multiple Agents
 For multi-UA orchestrations we can use a `pysipp.scenario`.
 The scenario from above is the default agent configuration:

From 7ade10cb11dfd0a220612e6bd09921a8f2fd3510 Mon Sep 17 00:00:00 2001
From: Konstantinos Tsakiltzidis <ktsakiltzidis@modulus.gr>
Date: Wed, 3 Jul 2019 12:06:14 +0300
Subject: [PATCH 08/23] Fix Authentication link in README

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 20cc728..1494dab 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ uac()  # run client synchronously
 ```
 
 ## Authentication
-When using the `[authentication]` (sipp keyword)[https://sipp.readthedocs.io/en/latest/scenarios/keywords.html#authentication]
+When using the `[authentication]` [sipp keyword](https://sipp.readthedocs.io/en/latest/scenarios/keywords.html#authentication)
 in scenarios, providing the credentials can be done with the
 `auth_username` and `auth_password` arguments, for example:
 

From 3c80ff13266b543ba2cb8964d1833114816ad50b Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Thu, 4 Jul 2019 14:54:23 -0400
Subject: [PATCH 09/23] Go easy on ol' `sippy_cup`

Resolves #48
---
 README.md | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 1494dab..e9dc7ad 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,8 @@ Python configuring and launching the infamous
 ## It doesn't try to
 
 - Auto-generate SIPp XML scripts like [sippy_cup](https://github.com/mojolingo/sippy_cup)
-  * we believe this is the wrong way to work around the problem of SIPp's shitty XML control language
+  * `pysipp` in no way tries to work around the problem of SIPp's awful
+    XML control language; your current scenario scripts are compatible!
 
 
 ## Basic Usage
@@ -175,7 +176,8 @@ pysipp.utils.log_to_stderr("DEBUG")
 ### Applying default settings
 For now see [#4](https://github.com/SIPp/pysipp/issues/4)
 
-More to come...
+## More to come?
+- document attributes / flags
 - writing plugins
 - using a `pysipp_conf.py`
 - remote execution

From 5d25d26da8a1e552e76d90d112c81ddd4157bd70 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Thu, 4 Jul 2019 14:55:33 -0400
Subject: [PATCH 10/23] Drop pypy from CI for now

---
 .travis.yml | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 144d9b6..1d07d66 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,8 +8,9 @@ python:
   - 2.7
   - 3.5
   - 3.6
+  # - 3.7
   - nightly
-  - pypy
+  # - pypy
   # - pypy3
 
 addons:
@@ -36,4 +37,4 @@ before_script:
   - cd ..
 
 script:
-  - pytest tests/
\ No newline at end of file
+  - pytest tests/

From 1adf41f18628eb35ba022dff803a3a5ce55c866a Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <tgoodlet@gmail.com>
Date: Fri, 12 Oct 2018 10:35:20 -0400
Subject: [PATCH 11/23] Add test for #36

Add a test which demonstrates a bug where if a scenario is loaded from
a directory that the `proxyaddr` is never set.
---
 tests/test_stack.py | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/tests/test_stack.py b/tests/test_stack.py
index 346df9c..3aa9b82 100644
--- a/tests/test_stack.py
+++ b/tests/test_stack.py
@@ -52,6 +52,25 @@ def test_confpy_hooks(scendir):
         assert agent.uri_username == 'doggy'
 
 
+def test_proxyaddr_with_scendir(scendir):
+    """When building a scenario from a xml file directory the
+    `proxyaddr` kwarg should be assigned.
+    """
+    remoteaddr = ('9.9.9.9', 80)
+    scen = pysipp.scenario(
+        dirpath=scendir + '/default_with_confpy',
+        proxyaddr=remoteaddr
+    )
+
+    assert scen.clientdefaults.proxyaddr == remoteaddr
+    for name, cmd in scen.cmditems():
+        if name == 'uac':
+            assert "-rsa '{}':'{}'".format(*remoteaddr) in cmd
+            assert "'{}':'{}'".format(*scen.clientdefaults.destaddr) in cmd
+        elif name == 'uas':
+            assert "-rsa '{}':'{}'".format(*remoteaddr) not in cmd
+
+
 def test_sync_run(scenwalk):
     """Ensure all scenarios in the test run to completion in synchronous mode
     """

From 79b75345aedded392ae71e83ad80d4dbbf0a31a0 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <tgoodlet@gmail.com>
Date: Fri, 12 Oct 2018 10:37:30 -0400
Subject: [PATCH 12/23] Fix #36

Thanks to @y-luis for originally finding and fixing this problem.
The original PR (#35) was discarded accidentally.
---
 pysipp/__init__.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pysipp/__init__.py b/pysipp/__init__.py
index 3e68a6b..46eb248 100644
--- a/pysipp/__init__.py
+++ b/pysipp/__init__.py
@@ -109,10 +109,10 @@ def scenario(dirpath=None, proxyaddr=None, autolocalsocks=True,
                 scenkwargs=scenkwargs
             )
 
-            if proxyaddr:
-                assert isinstance(
-                    proxyaddr, tuple), 'proxyaddr must be a (addr, port) tuple'
-                scen.clientdefaults.proxyaddr = proxyaddr
+    if proxyaddr:
+        assert isinstance(
+            proxyaddr, tuple), 'proxyaddr must be a (addr, port) tuple'
+        scen.clientdefaults.proxyaddr = proxyaddr
 
     return scen
 

From 9dddfe8bb7e7c01d08e2cbd7e7467b6d21d7cb06 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <tgoodlet@gmail.com>
Date: Fri, 12 Oct 2018 11:01:29 -0400
Subject: [PATCH 13/23] Add agent name to options merge logging

---
 pysipp/agent.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pysipp/agent.py b/pysipp/agent.py
index a57a312..2947755 100644
--- a/pysipp/agent.py
+++ b/pysipp/agent.py
@@ -362,10 +362,10 @@ def merge(dicts):
         # apply defaults
         ordered = [self._defaults, secondary, agent.todict()]
         for name, defs in zip(['defaults', dname, 'agent.todict()'], ordered):
-            log.debug("'{}' contents:\n{}".format(name, defs))
+            log.debug("{} '{}' contents:\n{}".format(agent.name, name, defs))
 
         params = merge(ordered)
-        log.debug("merged contents:\n{}".format(params))
+        log.debug("{} merged contents:\n{}".format(agent.name, params))
         ua = UserAgent(defaults=params)
 
         ua.enable_logging(enable_screen_file=self.enable_screen_file)

From e2ef59b16d0e59acef68b6792d2b98e1c13a3410 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 27 May 2019 09:49:40 -0400
Subject: [PATCH 14/23] Use pluggy 0.11.0

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 80128cd..a13dc99 100755
--- a/setup.py
+++ b/setup.py
@@ -36,7 +36,7 @@
     url='https://github.com/SIPp/pysipp',
     platforms=['linux'],
     packages=['pysipp', 'pysipp.cli'],
-    install_requires=['pluggy==0.3.1'],
+    install_requires=['pluggy==0.11.0'],
     tests_require=['pytest'],
     entry_points={
         'console_scripts': [

From 1e6f97c17e9315b5d1ebeeb2fcd30807db110074 Mon Sep 17 00:00:00 2001
From: Yunior Luis <yl.rojo@zaleos.net>
Date: Tue, 31 Jul 2018 18:58:42 +0200
Subject: [PATCH 15/23] Support disabling logging files.

---
 pysipp/agent.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/pysipp/agent.py b/pysipp/agent.py
index 2947755..7c1e303 100644
--- a/pysipp/agent.py
+++ b/pysipp/agent.py
@@ -235,7 +235,7 @@ class ScenarioType(object):
     """
 
     def __init__(self, agents, defaults, clientdefaults=None,
-                 serverdefaults=None, confpy=None, enable_screen_file=True):
+                 serverdefaults=None, confpy=None, logs=True, enable_screen_file=True):
         # agents iterable in launch-order
         self._agents = agents
         ua_attrs = UserAgent.keys()
@@ -255,6 +255,7 @@ def __init__(self, agents, defaults, clientdefaults=None,
         # hook module
         self.mod = confpy
         self.enable_screen_file = enable_screen_file
+        self.logs = logs
 
     @property
     def agents(self):
@@ -369,6 +370,8 @@ def merge(dicts):
         ua = UserAgent(defaults=params)
 
         ua.enable_logging(enable_screen_file=self.enable_screen_file)
+        if self.logs:
+            ua.enable_logging()
 
         # call post defaults hook
         plugin.mng.hook.pysipp_post_ua_defaults(ua=ua)

From 551042b32f8533df55cb761a7425f11424fb819f Mon Sep 17 00:00:00 2001
From: Yunior Luis <yluisrojo@gmail.com>
Date: Mon, 20 Aug 2018 16:58:35 +0200
Subject: [PATCH 16/23] Log sipp stdout.

---
 pysipp/launch.py | 19 +++++++++----------
 pysipp/report.py |  7 +++++++
 2 files changed, 16 insertions(+), 10 deletions(-)

diff --git a/pysipp/launch.py b/pysipp/launch.py
index a750128..9d63958 100644
--- a/pysipp/launch.py
+++ b/pysipp/launch.py
@@ -28,11 +28,12 @@ class PopenRunner(object):
 
     Adheres to an interface similar to `multiprocessing.pool.AsyncResult`.
     """
+
     def __init__(
-        self,
-        subprocmod=subprocess,
-        osmod=os,
-        poller=select.epoll,
+            self,
+            subprocmod=subprocess,
+            osmod=os,
+            poller=select.epoll,
     ):
         # these could optionally be rpyc proxy objs
         self.spm = subprocmod
@@ -53,8 +54,6 @@ def __call__(self, cmds, block=True, rate=300, **kwargs):
                 "Process results have not been cleared from previous run"
             )
         sp = self.spm
-        os = self.osm
-        DEVNULL = open(os.devnull, 'wb')
         fds2procs = OrderedDict()
 
         # run agent commands in sequence
@@ -63,15 +62,15 @@ def __call__(self, cmds, block=True, rate=300, **kwargs):
                 "launching cmd:\n\"{}\"\n".format(cmd))
             proc = sp.Popen(
                 shlex.split(cmd),
-                stdout=DEVNULL,
-                stderr=sp.PIPE
+                stdout=sp.PIPE,
+                stderr=sp.STDOUT
             )
-            fd = proc.stderr.fileno()
+            fd = proc.stdout.fileno()
             log.debug("registering fd '{}' for pid '{}'".format(
                 fd, proc.pid))
             fds2procs[fd] = self._procs[cmd] = proc
             # register for stderr hangup events
-            self.poller.register(proc.stderr.fileno(), select.EPOLLHUP)
+            self.poller.register(proc.stdout.fileno(), select.EPOLLHUP)
             # limit launch rate
             time.sleep(1. / rate)
 
diff --git a/pysipp/report.py b/pysipp/report.py
index 9643892..9fff885 100644
--- a/pysipp/report.py
+++ b/pysipp/report.py
@@ -54,6 +54,13 @@ def emit_logfiles(agents2procs, level='warn', max_lines=100):
         # logging mod bug?
         time.sleep(0.01)
 
+        # print stdout
+        emit("stdout for '{}' @ {}\n{}\n".format(
+            ua.name, ua.srcaddr, proc.streams.stdout))
+        # FIXME: no idea, but some logs are not being printed without this
+        # logging mod bug?
+        time.sleep(0.01)
+
         # print log file contents
         for name, fpath in ua.iter_toconsole_items():
             if fpath and path.isfile(fpath):

From d06379caf7bdc2aa3cb2a9bb2feb9e76148608cd Mon Sep 17 00:00:00 2001
From: "Y. Luis" <yluisrojo@gmail.com>
Date: Mon, 20 Aug 2018 17:25:49 +0100
Subject: [PATCH 17/23] Revert "Log sipp stdout."

---
 pysipp/launch.py | 19 ++++++++++---------
 pysipp/report.py |  7 -------
 2 files changed, 10 insertions(+), 16 deletions(-)

diff --git a/pysipp/launch.py b/pysipp/launch.py
index 9d63958..a750128 100644
--- a/pysipp/launch.py
+++ b/pysipp/launch.py
@@ -28,12 +28,11 @@ class PopenRunner(object):
 
     Adheres to an interface similar to `multiprocessing.pool.AsyncResult`.
     """
-
     def __init__(
-            self,
-            subprocmod=subprocess,
-            osmod=os,
-            poller=select.epoll,
+        self,
+        subprocmod=subprocess,
+        osmod=os,
+        poller=select.epoll,
     ):
         # these could optionally be rpyc proxy objs
         self.spm = subprocmod
@@ -54,6 +53,8 @@ def __call__(self, cmds, block=True, rate=300, **kwargs):
                 "Process results have not been cleared from previous run"
             )
         sp = self.spm
+        os = self.osm
+        DEVNULL = open(os.devnull, 'wb')
         fds2procs = OrderedDict()
 
         # run agent commands in sequence
@@ -62,15 +63,15 @@ def __call__(self, cmds, block=True, rate=300, **kwargs):
                 "launching cmd:\n\"{}\"\n".format(cmd))
             proc = sp.Popen(
                 shlex.split(cmd),
-                stdout=sp.PIPE,
-                stderr=sp.STDOUT
+                stdout=DEVNULL,
+                stderr=sp.PIPE
             )
-            fd = proc.stdout.fileno()
+            fd = proc.stderr.fileno()
             log.debug("registering fd '{}' for pid '{}'".format(
                 fd, proc.pid))
             fds2procs[fd] = self._procs[cmd] = proc
             # register for stderr hangup events
-            self.poller.register(proc.stdout.fileno(), select.EPOLLHUP)
+            self.poller.register(proc.stderr.fileno(), select.EPOLLHUP)
             # limit launch rate
             time.sleep(1. / rate)
 
diff --git a/pysipp/report.py b/pysipp/report.py
index 9fff885..9643892 100644
--- a/pysipp/report.py
+++ b/pysipp/report.py
@@ -54,13 +54,6 @@ def emit_logfiles(agents2procs, level='warn', max_lines=100):
         # logging mod bug?
         time.sleep(0.01)
 
-        # print stdout
-        emit("stdout for '{}' @ {}\n{}\n".format(
-            ua.name, ua.srcaddr, proc.streams.stdout))
-        # FIXME: no idea, but some logs are not being printed without this
-        # logging mod bug?
-        time.sleep(0.01)
-
         # print log file contents
         for name, fpath in ua.iter_toconsole_items():
             if fpath and path.isfile(fpath):

From 650eec18b492e7ddcca089cefde282906276d2e3 Mon Sep 17 00:00:00 2001
From: Yunior Luis <yluisrojo@gmail.com>
Date: Mon, 20 Aug 2018 20:00:21 +0200
Subject: [PATCH 18/23] Beta release.

---
 setup.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/setup.py b/setup.py
index a13dc99..b932c6a 100755
--- a/setup.py
+++ b/setup.py
@@ -26,7 +26,7 @@
 
 setup(
     name="pysipp",
-    version='0.1.alpha',
+    version='0.1.beta',
     description='pysipp is a SIPp scenario launcher for for use in'
                 ' automated VoIP testing',
     long_description=readme,
@@ -44,7 +44,7 @@
         ],
     },
     classifiers=[
-        'Development Status :: 3 - Alpha',
+        'Development Status :: 4 - Beta',
         'Intended Audience :: Developers',
         'License :: OSI Approved :: GNU General Public License v2',
         'Operating System :: Linux',

From e890e23ae0bd37472c449de409d8653976860531 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <tgoodlet@gmail.com>
Date: Fri, 17 Jun 2016 18:37:21 -0400
Subject: [PATCH 19/23] Use a plain old select loop

Use `select.select` to track subproc completion since `select.epoll`
isn't available on OSX.

Resolves #16
---
 pysipp/launch.py | 44 +++++++++++++++++++++++++++++++++-----------
 1 file changed, 33 insertions(+), 11 deletions(-)

diff --git a/pysipp/launch.py b/pysipp/launch.py
index a750128..ddb3131 100644
--- a/pysipp/launch.py
+++ b/pysipp/launch.py
@@ -32,12 +32,12 @@ def __init__(
         self,
         subprocmod=subprocess,
         osmod=os,
-        poller=select.epoll,
+        select=select.select,
     ):
         # these could optionally be rpyc proxy objs
         self.spm = subprocmod
         self.osm = osmod
-        self.poller = poller()
+        self.select = select
         # collector thread placeholder
         self._waiter = None
         # store proc results
@@ -67,11 +67,9 @@ def __call__(self, cmds, block=True, rate=300, **kwargs):
                 stderr=sp.PIPE
             )
             fd = proc.stderr.fileno()
-            log.debug("registering fd '{}' for pid '{}'".format(
-                fd, proc.pid))
+            log.debug("registering fd '{}' for pid '{}'".format(fd, proc.pid))
             fds2procs[fd] = self._procs[cmd] = proc
-            # register for stderr hangup events
-            self.poller.register(proc.stderr.fileno(), select.EPOLLHUP)
+
             # limit launch rate
             time.sleep(1. / rate)
 
@@ -87,15 +85,39 @@ def _wait(self, fds2procs):
         signalled = None
         left = len(fds2procs)
         collected = 0
+        p2fd = {p: p.stderr for p in fds2procs.values()}
+        stderrs = {p: [] for p in fds2procs.values()}
+
+        # wait on stderr hangup events
         while collected < left:
-            pairs = self.poller.poll()  # wait on hangup events
-            log.debug("received hangup for pairs '{}'".format(pairs))
-            for fd, status in pairs:
+            try:
+                fds, _, _ = self.select(list(p2fd.values()), [], [])
+            except ValueError:
+                # all fds are now closed
+                hungup = p2fd.keys()
+            else:
+                hungup = []
+                for proc in (fds2procs[fd.fileno()] for fd in fds):
+                    data = proc.stderr.read()
+                    if data != '':
+                        stderrs[proc].append(data)  # append stderr
+                        continue
+
+                    p2fd.pop(proc)
+                    hungup.append(proc)
+
+                if not hungup:
+                    continue
+
+            for proc in hungup:
+                log.debug("received hangup for pid '{}'".format(proc.pid))
                 collected += 1
-                proc = fds2procs[fd]
                 # attach streams so they can be read more then once
                 log.debug("collecting streams for {}".format(proc))
-                proc.streams = Streams(*proc.communicate())  # timeout=2))
+                proc.streams = Streams(
+                    stdout=proc.communicate()[0],
+                    stderr=''.join(stderrs[proc]),
+                )  # timeout=2))
                 if proc.returncode != 0 and not signalled:
                     # stop all other agents if there is a failure
                     signalled = self.stop()

From 611ead886cd8bcfbe04dcb95189baa173eebf2c5 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <tgoodlet@gmail.com>
Date: Fri, 17 Jun 2016 18:40:22 -0400
Subject: [PATCH 20/23] Tweak unit test exit code checking

---
 tests/test_launcher.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/test_launcher.py b/tests/test_launcher.py
index e1d435a..48e2dc9 100644
--- a/tests/test_launcher.py
+++ b/tests/test_launcher.py
@@ -29,12 +29,12 @@ def test_agent_fails():
     assert uasproc.streams.stderr
     assert uasproc.returncode == 255, uasproc.streams.stderr
 
-    # killed by signal
+    # times out (can't do by signal - SIPp issue #176)
     uacproc = runner.get(timeout=0)[uac.render()]
     # assert not uacproc.streams.stderr  # sometimes this has a log msg?
     ret = uacproc.returncode
-    # killed by SIGUSR1 or terminates before it starts (racy)
-    assert ret == -10 or ret == 0
+    # timed out or terminates before it starts (racy)
+    assert ret == -10 or ret == 0 or ret == 1
 
 
 def test_default_scen(default_agents):

From d522d6b1dd0cedcd5eaec6cb17983127c27b7148 Mon Sep 17 00:00:00 2001
From: Yunior Luis <yluisrojo@gmail.com>
Date: Sun, 14 Oct 2018 19:21:53 +0200
Subject: [PATCH 21/23] Fix default host retrieving random socket from local
 OS.

---
 pysipp/netplug.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pysipp/netplug.py b/pysipp/netplug.py
index 4d2e87a..8004b1c 100644
--- a/pysipp/netplug.py
+++ b/pysipp/netplug.py
@@ -10,12 +10,12 @@ def getsockaddr(host, family=socket.AF_INET, port=0, sockmod=socket):
     binding to an ip, acquiring a random port and then
     closing the socket and returning that address.
 
-    ..warning:: Obviously this is not guarateed to be an unused address
+    ..warning:: Obviously this is not guaranteed to be an unused address
         since we don't actually keep it bound, so there may be a race with
         other processes acquiring the addr before our SIPp process re-binds.
     """
     for fam, stype, proto, _, sa in socket.getaddrinfo(
-        host, port, family, socket.SOCK_DGRAM, 0, socket.AI_PASSIVE,
+            host if host else '127.0.0.1', port, family, socket.SOCK_DGRAM, 0, socket.AI_PASSIVE,
     ):
         s = socket.socket(family, stype, proto)
         s.bind(sa)

From 00694f34a2c513359086d0275bf94adc9b32f8de Mon Sep 17 00:00:00 2001
From: Yunior Luis <yluisrojo@gmail.com>
Date: Sun, 14 Oct 2018 19:22:35 +0200
Subject: [PATCH 22/23] Git ignore PyCharm and pytest related folders.

---
 .gitignore | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/.gitignore b/.gitignore
index ba74660..a32a803 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,6 +42,7 @@ htmlcov/
 nosetests.xml
 coverage.xml
 *,cover
+.pytest_cache
 
 # Translations
 *.mo
@@ -55,3 +56,6 @@ docs/_build/
 
 # PyBuilder
 target/
+
+# PyCharm
+.idea/
\ No newline at end of file

From 466ffa4547dc01c0397baef616422b3413d69c2e Mon Sep 17 00:00:00 2001
From: Luis Rojo <yluisrojo@gmail.com>
Date: Fri, 5 Jul 2019 13:56:23 +0100
Subject: [PATCH 23/23] Revert setup status

---
 setup.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/setup.py b/setup.py
index b932c6a..a13dc99 100755
--- a/setup.py
+++ b/setup.py
@@ -26,7 +26,7 @@
 
 setup(
     name="pysipp",
-    version='0.1.beta',
+    version='0.1.alpha',
     description='pysipp is a SIPp scenario launcher for for use in'
                 ' automated VoIP testing',
     long_description=readme,
@@ -44,7 +44,7 @@
         ],
     },
     classifiers=[
-        'Development Status :: 4 - Beta',
+        'Development Status :: 3 - Alpha',
         'Intended Audience :: Developers',
         'License :: OSI Approved :: GNU General Public License v2',
         'Operating System :: Linux',