We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
IPP controller port_range option doesn't constraint all TCP ports needed by IPP 
Hello,

I tried using Parsl (0.6.1) on a condor cluster with TCP ports e.g.: 9100-9800 open using the IPyParallel executor with the [port_range](https://github.com/Parsl/parsl/blob/master/parsl/executors/ipp_controller.py#L28-L30) option.

However, while the JSON created does choose a random port for registration, it doesn't do the same for other TCP ports like the hubFactory control, mux, task, etc. Example in [1] (where `port_range='9100,9800'`).

It turns out  `ipcontroller` [allows](https://stackoverflow.com/questions/23145650/how-to-setup-ssh-tunnel-for-ipython-cluster-ipcluster) to define these parameters via `--HubFactory.<task, mux, control, etc>` , so I just created a controller that randomly selects all these ports from `port_range`, which seems to be working fine for me. It would be nice to have this feature into the IPP controller itself though.

https://github.com/vc3-project/vc3-playbooks/blob/master/login/jupyter_files/parsl/vc3controller.py#L52-L58  

```
[1]
cat <<EOF > ipengine.a77ce9a9-f742-41ce-afb4-e2b6c7058e78.json
{
  "ssh": "",
  "interface": "tcp://*",
  "registration": 9560,
  "control": 33229,
  "mux": 44249,
  "hb_ping": 41099,
  "hb_pong": 25087,
  "task": 43484,
  "iopub": 54747,
  "key": "808521cb-95a5a73d5c388767b23c668a",
  "location": "128.135.158.176",
  "pack": "json",
  "unpack": "json",
  "signature_scheme": "hmac-sha256"
}
```
 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `parsl/executors/ipp_controller.py`
Content:
```
1 import logging
2 import os
3 import random
4 import signal
5 import subprocess
6 import time
7 
8 from parsl.executors.errors import *
9 from parsl.utils import RepresentationMixin
10 
11 logger = logging.getLogger(__name__)
12 
13 
14 class Controller(RepresentationMixin):
15     """Start and maintain a IPythonParallel controller.
16 
17     Parameters
18     ----------
19 
20     public_ip : str, optional
21         Specific IP address of the controller, as seen from the engines. If `None`, an attempt will
22         be made to guess the correct value. Default is None.
23     interfaces : str, optional
24         Interfaces for ZeroMQ to listen on. Default is "*".
25     port : int or str, optional
26         Port on which the iPython hub listens for registration. If set to `None`, the IPython default will be used. Default
27         is None.
28     port_range : str, optional
29         The minimum and maximum port value, in the format '<min>,<max>' (for example: '50000,60000'. If this does not equal
30         None, a random port in `port_range` will be selected.
31     reuse : bool, optional
32         Reuse an existing controller.
33     ipython_dir : str, optional
34         IPython directory for IPythonParallel to store config files. This will be overriden by the auto controller
35         start. Default is "~/.ipython".
36     profile : str, optional
37         Path to an IPython profile to use. Default is 'default'.
38     mode : str, optional
39         If "auto", controller will be created and managed automatically. If "manual" the controller
40         is assumed to be created by the user. Default is auto.
41     """
42     def __init__(self, public_ip=None, interfaces=None, port=None, port_range=None, reuse=False,
43                  log=True, ipython_dir="~/.ipython", mode="auto", profile='default'):
44         self.public_ip = public_ip
45         self.interfaces = interfaces
46         self.port = port
47         self.port_range = port_range
48         if port_range is not None:
49             port_min, port_max = [int(x) for x in port_range.split(',')]
50             self.port = random.randint(port_min, port_max)
51         self.reuse = reuse
52         self.log = log
53         self.ipython_dir = ipython_dir
54         self.mode = mode
55         self.profile = profile
56 
57     def start(self):
58         """Start the controller."""
59 
60         if self.mode == "manual":
61             return
62 
63         if self.ipython_dir is not '~/.ipython':
64             self.ipython_dir = os.path.abspath(os.path.expanduser(self.ipython_dir))
65 
66         if self.log:
67             stdout = open(os.path.join(self.ipython_dir, "{0}.controller.out".format(self.profile)), 'w')
68             stderr = open(os.path.join(self.ipython_dir, "{0}.controller.err".format(self.profile)), 'w')
69         else:
70             stdout = open(os.devnull, 'w')
71             stderr = open(os.devnull, 'w')
72 
73         try:
74             opts = [
75                 'ipcontroller',
76                 '' if self.ipython_dir is '~/.ipython' else '--ipython-dir={}'.format(self.ipython_dir),
77                 self.interfaces if self.interfaces is not None else '--ip=*',
78                 '' if self.profile is 'default' else '--profile={0}'.format(self.profile),
79                 '--reuse' if self.reuse else '',
80                 '--port={}'.format(self.port) if self.port is not None else '',
81                 '--location={}'.format(self.public_ip) if self.public_ip else ''
82             ]
83             logger.debug("Starting ipcontroller with '{}'".format(' '.join([str(x) for x in opts])))
84             self.proc = subprocess.Popen(opts, stdout=stdout, stderr=stderr, preexec_fn=os.setsid)
85         except FileNotFoundError:
86             msg = "Could not find ipcontroller. Please make sure that ipyparallel is installed and available in your env"
87             logger.error(msg)
88             raise ControllerError(msg)
89         except Exception as e:
90             msg = "IPPController failed to start: {0}".format(e)
91             logger.error(msg)
92             raise ControllerError(msg)
93 
94     @property
95     def engine_file(self):
96         """Specify path to the ipcontroller-engine.json file.
97 
98         This file is stored in in the ipython_dir/profile folders.
99 
100         Returns :
101               - str, File path to engine file
102         """
103         return os.path.join(self.ipython_dir,
104                             'profile_{0}'.format(self.profile),
105                             'security/ipcontroller-engine.json')
106 
107     @property
108     def client_file(self):
109         """Specify path to the ipcontroller-client.json file.
110 
111         This file is stored in in the ipython_dir/profile folders.
112 
113         Returns :
114               - str, File path to client file
115         """
116         return os.path.join(self.ipython_dir,
117                             'profile_{0}'.format(self.profile),
118                             'security/ipcontroller-client.json')
119 
120     def close(self):
121         """Terminate the controller process and its child processes.
122 
123         Args:
124               - None
125         """
126         if self.reuse:
127             logger.debug("Ipcontroller not shutting down: reuse enabled")
128             return
129 
130         if self.mode == "manual":
131             logger.debug("Ipcontroller not shutting down: Manual mode")
132             return
133 
134         try:
135             pgid = os.getpgid(self.proc.pid)
136             os.killpg(pgid, signal.SIGTERM)
137             time.sleep(0.2)
138             os.killpg(pgid, signal.SIGKILL)
139             try:
140                 self.proc.wait(timeout=1)
141                 x = self.proc.returncode
142                 if x == 0:
143                     logger.debug("Controller exited with {0}".format(x))
144                 else:
145                     logger.error("Controller exited with {0}. May require manual cleanup".format(x))
146             except subprocess.TimeoutExpired:
147                 logger.warn("Ipcontroller process:{0} cleanup failed. May require manual cleanup".format(self.proc.pid))
148 
149         except Exception as e:
150             logger.warn("Failed to kill the ipcontroller process[{0}]: {1}".format(self.proc.pid, e))
151
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/parsl/executors/ipp_controller.py b/parsl/executors/ipp_controller.py
--- a/parsl/executors/ipp_controller.py
+++ b/parsl/executors/ipp_controller.py
@@ -26,8 +26,9 @@
         Port on which the iPython hub listens for registration. If set to `None`, the IPython default will be used. Default
         is None.
     port_range : str, optional
-        The minimum and maximum port value, in the format '<min>,<max>' (for example: '50000,60000'. If this does not equal
-        None, a random port in `port_range` will be selected.
+        The minimum and maximum port values to use, in the format '<min>,<max>' (for example: '50000,60000').
+        If this does not equal None, random ports in `port_range` will be selected for all HubFactory listening services.
+        This option overrides the port setting value for registration.
     reuse : bool, optional
         Reuse an existing controller.
     ipython_dir : str, optional
@@ -45,9 +46,17 @@
         self.interfaces = interfaces
         self.port = port
         self.port_range = port_range
+
         if port_range is not None:
             port_min, port_max = [int(x) for x in port_range.split(',')]
-            self.port = random.randint(port_min, port_max)
+            (
+                self.port,
+                self.hb_ping, self.hb_pong,
+                self.control_client, self.control_engine,
+                self.mux_client, self.mux_engine,
+                self.task_client, self.task_engine
+            ) = random.sample(range(port_min, port_max), 9)
+
         self.reuse = reuse
         self.log = log
         self.ipython_dir = ipython_dir
@@ -77,9 +86,16 @@
                 self.interfaces if self.interfaces is not None else '--ip=*',
                 '' if self.profile is 'default' else '--profile={0}'.format(self.profile),
                 '--reuse' if self.reuse else '',
-                '--port={}'.format(self.port) if self.port is not None else '',
-                '--location={}'.format(self.public_ip) if self.public_ip else ''
+                '--location={}'.format(self.public_ip) if self.public_ip else '',
+                '--port={}'.format(self.port) if self.port is not None else ''
             ]
+            if self.port_range is not None:
+                opts += [
+                    '--HubFactory.hb={0},{1}'.format(self.hb_ping, self.hb_pong),
+                    '--HubFactory.control={0},{1}'.format(self.control_client, self.control_engine),
+                    '--HubFactory.mux={0},{1}'.format(self.mux_client, self.mux_engine),
+                    '--HubFactory.task={0},{1}'.format(self.task_client, self.task_engine)
+                ]
             logger.debug("Starting ipcontroller with '{}'".format(' '.join([str(x) for x in opts])))
             self.proc = subprocess.Popen(opts, stdout=stdout, stderr=stderr, preexec_fn=os.setsid)
         except FileNotFoundError:

{"golden_diff": "diff --git a/parsl/executors/ipp_controller.py b/parsl/executors/ipp_controller.py\n--- a/parsl/executors/ipp_controller.py\n+++ b/parsl/executors/ipp_controller.py\n@@ -26,8 +26,9 @@\n         Port on which the iPython hub listens for registration. If set to `None`, the IPython default will be used. Default\n         is None.\n     port_range : str, optional\n-        The minimum and maximum port value, in the format '<min>,<max>' (for example: '50000,60000'. If this does not equal\n-        None, a random port in `port_range` will be selected.\n+        The minimum and maximum port values to use, in the format '<min>,<max>' (for example: '50000,60000').\n+        If this does not equal None, random ports in `port_range` will be selected for all HubFactory listening services.\n+        This option overrides the port setting value for registration.\n     reuse : bool, optional\n         Reuse an existing controller.\n     ipython_dir : str, optional\n@@ -45,9 +46,17 @@\n         self.interfaces = interfaces\n         self.port = port\n         self.port_range = port_range\n+\n         if port_range is not None:\n             port_min, port_max = [int(x) for x in port_range.split(',')]\n-            self.port = random.randint(port_min, port_max)\n+            (\n+                self.port,\n+                self.hb_ping, self.hb_pong,\n+                self.control_client, self.control_engine,\n+                self.mux_client, self.mux_engine,\n+                self.task_client, self.task_engine\n+            ) = random.sample(range(port_min, port_max), 9)\n+\n         self.reuse = reuse\n         self.log = log\n         self.ipython_dir = ipython_dir\n@@ -77,9 +86,16 @@\n                 self.interfaces if self.interfaces is not None else '--ip=*',\n                 '' if self.profile is 'default' else '--profile={0}'.format(self.profile),\n                 '--reuse' if self.reuse else '',\n-                '--port={}'.format(self.port) if self.port is not None else '',\n-                '--location={}'.format(self.public_ip) if self.public_ip else ''\n+                '--location={}'.format(self.public_ip) if self.public_ip else '',\n+                '--port={}'.format(self.port) if self.port is not None else ''\n             ]\n+            if self.port_range is not None:\n+                opts += [\n+                    '--HubFactory.hb={0},{1}'.format(self.hb_ping, self.hb_pong),\n+                    '--HubFactory.control={0},{1}'.format(self.control_client, self.control_engine),\n+                    '--HubFactory.mux={0},{1}'.format(self.mux_client, self.mux_engine),\n+                    '--HubFactory.task={0},{1}'.format(self.task_client, self.task_engine)\n+                ]\n             logger.debug(\"Starting ipcontroller with '{}'\".format(' '.join([str(x) for x in opts])))\n             self.proc = subprocess.Popen(opts, stdout=stdout, stderr=stderr, preexec_fn=os.setsid)\n         except FileNotFoundError:\n", "issue": "IPP controller port_range option doesn't constraint all TCP ports needed by IPP \nHello,\r\n\r\nI tried using Parsl (0.6.1) on a condor cluster with TCP ports e.g.: 9100-9800 open using the IPyParallel executor with the [port_range](https://github.com/Parsl/parsl/blob/master/parsl/executors/ipp_controller.py#L28-L30) option.\r\n\r\nHowever, while the JSON created does choose a random port for registration, it doesn't do the same for other TCP ports like the hubFactory control, mux, task, etc. Example in [1] (where `port_range='9100,9800'`).\r\n\r\nIt turns out  `ipcontroller` [allows](https://stackoverflow.com/questions/23145650/how-to-setup-ssh-tunnel-for-ipython-cluster-ipcluster) to define these parameters via `--HubFactory.<task, mux, control, etc>` , so I just created a controller that randomly selects all these ports from `port_range`, which seems to be working fine for me. It would be nice to have this feature into the IPP controller itself though.\r\n\r\nhttps://github.com/vc3-project/vc3-playbooks/blob/master/login/jupyter_files/parsl/vc3controller.py#L52-L58  \r\n\r\n```\r\n[1]\r\ncat <<EOF > ipengine.a77ce9a9-f742-41ce-afb4-e2b6c7058e78.json\r\n{\r\n  \"ssh\": \"\",\r\n  \"interface\": \"tcp://*\",\r\n  \"registration\": 9560,\r\n  \"control\": 33229,\r\n  \"mux\": 44249,\r\n  \"hb_ping\": 41099,\r\n  \"hb_pong\": 25087,\r\n  \"task\": 43484,\r\n  \"iopub\": 54747,\r\n  \"key\": \"808521cb-95a5a73d5c388767b23c668a\",\r\n  \"location\": \"128.135.158.176\",\r\n  \"pack\": \"json\",\r\n  \"unpack\": \"json\",\r\n  \"signature_scheme\": \"hmac-sha256\"\r\n}\r\n```\r\n \n", "before_files": [{"content": "import logging\nimport os\nimport random\nimport signal\nimport subprocess\nimport time\n\nfrom parsl.executors.errors import *\nfrom parsl.utils import RepresentationMixin\n\nlogger = logging.getLogger(__name__)\n\n\nclass Controller(RepresentationMixin):\n    \"\"\"Start and maintain a IPythonParallel controller.\n\n    Parameters\n    ----------\n\n    public_ip : str, optional\n        Specific IP address of the controller, as seen from the engines. If `None`, an attempt will\n        be made to guess the correct value. Default is None.\n    interfaces : str, optional\n        Interfaces for ZeroMQ to listen on. Default is \"*\".\n    port : int or str, optional\n        Port on which the iPython hub listens for registration. If set to `None`, the IPython default will be used. Default\n        is None.\n    port_range : str, optional\n        The minimum and maximum port value, in the format '<min>,<max>' (for example: '50000,60000'. If this does not equal\n        None, a random port in `port_range` will be selected.\n    reuse : bool, optional\n        Reuse an existing controller.\n    ipython_dir : str, optional\n        IPython directory for IPythonParallel to store config files. This will be overriden by the auto controller\n        start. Default is \"~/.ipython\".\n    profile : str, optional\n        Path to an IPython profile to use. Default is 'default'.\n    mode : str, optional\n        If \"auto\", controller will be created and managed automatically. If \"manual\" the controller\n        is assumed to be created by the user. Default is auto.\n    \"\"\"\n    def __init__(self, public_ip=None, interfaces=None, port=None, port_range=None, reuse=False,\n                 log=True, ipython_dir=\"~/.ipython\", mode=\"auto\", profile='default'):\n        self.public_ip = public_ip\n        self.interfaces = interfaces\n        self.port = port\n        self.port_range = port_range\n        if port_range is not None:\n            port_min, port_max = [int(x) for x in port_range.split(',')]\n            self.port = random.randint(port_min, port_max)\n        self.reuse = reuse\n        self.log = log\n        self.ipython_dir = ipython_dir\n        self.mode = mode\n        self.profile = profile\n\n    def start(self):\n        \"\"\"Start the controller.\"\"\"\n\n        if self.mode == \"manual\":\n            return\n\n        if self.ipython_dir is not '~/.ipython':\n            self.ipython_dir = os.path.abspath(os.path.expanduser(self.ipython_dir))\n\n        if self.log:\n            stdout = open(os.path.join(self.ipython_dir, \"{0}.controller.out\".format(self.profile)), 'w')\n            stderr = open(os.path.join(self.ipython_dir, \"{0}.controller.err\".format(self.profile)), 'w')\n        else:\n            stdout = open(os.devnull, 'w')\n            stderr = open(os.devnull, 'w')\n\n        try:\n            opts = [\n                'ipcontroller',\n                '' if self.ipython_dir is '~/.ipython' else '--ipython-dir={}'.format(self.ipython_dir),\n                self.interfaces if self.interfaces is not None else '--ip=*',\n                '' if self.profile is 'default' else '--profile={0}'.format(self.profile),\n                '--reuse' if self.reuse else '',\n                '--port={}'.format(self.port) if self.port is not None else '',\n                '--location={}'.format(self.public_ip) if self.public_ip else ''\n            ]\n            logger.debug(\"Starting ipcontroller with '{}'\".format(' '.join([str(x) for x in opts])))\n            self.proc = subprocess.Popen(opts, stdout=stdout, stderr=stderr, preexec_fn=os.setsid)\n        except FileNotFoundError:\n            msg = \"Could not find ipcontroller. Please make sure that ipyparallel is installed and available in your env\"\n            logger.error(msg)\n            raise ControllerError(msg)\n        except Exception as e:\n            msg = \"IPPController failed to start: {0}\".format(e)\n            logger.error(msg)\n            raise ControllerError(msg)\n\n    @property\n    def engine_file(self):\n        \"\"\"Specify path to the ipcontroller-engine.json file.\n\n        This file is stored in in the ipython_dir/profile folders.\n\n        Returns :\n              - str, File path to engine file\n        \"\"\"\n        return os.path.join(self.ipython_dir,\n                            'profile_{0}'.format(self.profile),\n                            'security/ipcontroller-engine.json')\n\n    @property\n    def client_file(self):\n        \"\"\"Specify path to the ipcontroller-client.json file.\n\n        This file is stored in in the ipython_dir/profile folders.\n\n        Returns :\n              - str, File path to client file\n        \"\"\"\n        return os.path.join(self.ipython_dir,\n                            'profile_{0}'.format(self.profile),\n                            'security/ipcontroller-client.json')\n\n    def close(self):\n        \"\"\"Terminate the controller process and its child processes.\n\n        Args:\n              - None\n        \"\"\"\n        if self.reuse:\n            logger.debug(\"Ipcontroller not shutting down: reuse enabled\")\n            return\n\n        if self.mode == \"manual\":\n            logger.debug(\"Ipcontroller not shutting down: Manual mode\")\n            return\n\n        try:\n            pgid = os.getpgid(self.proc.pid)\n            os.killpg(pgid, signal.SIGTERM)\n            time.sleep(0.2)\n            os.killpg(pgid, signal.SIGKILL)\n            try:\n                self.proc.wait(timeout=1)\n                x = self.proc.returncode\n                if x == 0:\n                    logger.debug(\"Controller exited with {0}\".format(x))\n                else:\n                    logger.error(\"Controller exited with {0}. May require manual cleanup\".format(x))\n            except subprocess.TimeoutExpired:\n                logger.warn(\"Ipcontroller process:{0} cleanup failed. May require manual cleanup\".format(self.proc.pid))\n\n        except Exception as e:\n            logger.warn(\"Failed to kill the ipcontroller process[{0}]: {1}\".format(self.proc.pid, e))\n", "path": "parsl/executors/ipp_controller.py"}], "after_files": [{"content": "import logging\nimport os\nimport random\nimport signal\nimport subprocess\nimport time\n\nfrom parsl.executors.errors import *\nfrom parsl.utils import RepresentationMixin\n\nlogger = logging.getLogger(__name__)\n\n\nclass Controller(RepresentationMixin):\n    \"\"\"Start and maintain a IPythonParallel controller.\n\n    Parameters\n    ----------\n\n    public_ip : str, optional\n        Specific IP address of the controller, as seen from the engines. If `None`, an attempt will\n        be made to guess the correct value. Default is None.\n    interfaces : str, optional\n        Interfaces for ZeroMQ to listen on. Default is \"*\".\n    port : int or str, optional\n        Port on which the iPython hub listens for registration. If set to `None`, the IPython default will be used. Default\n        is None.\n    port_range : str, optional\n        The minimum and maximum port values to use, in the format '<min>,<max>' (for example: '50000,60000').\n        If this does not equal None, random ports in `port_range` will be selected for all HubFactory listening services.\n        This option overrides the port setting value for registration.\n    reuse : bool, optional\n        Reuse an existing controller.\n    ipython_dir : str, optional\n        IPython directory for IPythonParallel to store config files. This will be overriden by the auto controller\n        start. Default is \"~/.ipython\".\n    profile : str, optional\n        Path to an IPython profile to use. Default is 'default'.\n    mode : str, optional\n        If \"auto\", controller will be created and managed automatically. If \"manual\" the controller\n        is assumed to be created by the user. Default is auto.\n    \"\"\"\n    def __init__(self, public_ip=None, interfaces=None, port=None, port_range=None, reuse=False,\n                 log=True, ipython_dir=\"~/.ipython\", mode=\"auto\", profile='default'):\n        self.public_ip = public_ip\n        self.interfaces = interfaces\n        self.port = port\n        self.port_range = port_range\n\n        if port_range is not None:\n            port_min, port_max = [int(x) for x in port_range.split(',')]\n            (\n                self.port,\n                self.hb_ping, self.hb_pong,\n                self.control_client, self.control_engine,\n                self.mux_client, self.mux_engine,\n                self.task_client, self.task_engine\n            ) = random.sample(range(port_min, port_max), 9)\n\n        self.reuse = reuse\n        self.log = log\n        self.ipython_dir = ipython_dir\n        self.mode = mode\n        self.profile = profile\n\n    def start(self):\n        \"\"\"Start the controller.\"\"\"\n\n        if self.mode == \"manual\":\n            return\n\n        if self.ipython_dir is not '~/.ipython':\n            self.ipython_dir = os.path.abspath(os.path.expanduser(self.ipython_dir))\n\n        if self.log:\n            stdout = open(os.path.join(self.ipython_dir, \"{0}.controller.out\".format(self.profile)), 'w')\n            stderr = open(os.path.join(self.ipython_dir, \"{0}.controller.err\".format(self.profile)), 'w')\n        else:\n            stdout = open(os.devnull, 'w')\n            stderr = open(os.devnull, 'w')\n\n        try:\n            opts = [\n                'ipcontroller',\n                '' if self.ipython_dir is '~/.ipython' else '--ipython-dir={}'.format(self.ipython_dir),\n                self.interfaces if self.interfaces is not None else '--ip=*',\n                '' if self.profile is 'default' else '--profile={0}'.format(self.profile),\n                '--reuse' if self.reuse else '',\n                '--location={}'.format(self.public_ip) if self.public_ip else '',\n                '--port={}'.format(self.port) if self.port is not None else ''\n            ]\n            if self.port_range is not None:\n                opts += [\n                    '--HubFactory.hb={0},{1}'.format(self.hb_ping, self.hb_pong),\n                    '--HubFactory.control={0},{1}'.format(self.control_client, self.control_engine),\n                    '--HubFactory.mux={0},{1}'.format(self.mux_client, self.mux_engine),\n                    '--HubFactory.task={0},{1}'.format(self.task_client, self.task_engine)\n                ]\n            logger.debug(\"Starting ipcontroller with '{}'\".format(' '.join([str(x) for x in opts])))\n            self.proc = subprocess.Popen(opts, stdout=stdout, stderr=stderr, preexec_fn=os.setsid)\n        except FileNotFoundError:\n            msg = \"Could not find ipcontroller. Please make sure that ipyparallel is installed and available in your env\"\n            logger.error(msg)\n            raise ControllerError(msg)\n        except Exception as e:\n            msg = \"IPPController failed to start: {0}\".format(e)\n            logger.error(msg)\n            raise ControllerError(msg)\n\n    @property\n    def engine_file(self):\n        \"\"\"Specify path to the ipcontroller-engine.json file.\n\n        This file is stored in in the ipython_dir/profile folders.\n\n        Returns :\n              - str, File path to engine file\n        \"\"\"\n        return os.path.join(self.ipython_dir,\n                            'profile_{0}'.format(self.profile),\n                            'security/ipcontroller-engine.json')\n\n    @property\n    def client_file(self):\n        \"\"\"Specify path to the ipcontroller-client.json file.\n\n        This file is stored in in the ipython_dir/profile folders.\n\n        Returns :\n              - str, File path to client file\n        \"\"\"\n        return os.path.join(self.ipython_dir,\n                            'profile_{0}'.format(self.profile),\n                            'security/ipcontroller-client.json')\n\n    def close(self):\n        \"\"\"Terminate the controller process and its child processes.\n\n        Args:\n              - None\n        \"\"\"\n        if self.reuse:\n            logger.debug(\"Ipcontroller not shutting down: reuse enabled\")\n            return\n\n        if self.mode == \"manual\":\n            logger.debug(\"Ipcontroller not shutting down: Manual mode\")\n            return\n\n        try:\n            pgid = os.getpgid(self.proc.pid)\n            os.killpg(pgid, signal.SIGTERM)\n            time.sleep(0.2)\n            os.killpg(pgid, signal.SIGKILL)\n            try:\n                self.proc.wait(timeout=1)\n                x = self.proc.returncode\n                if x == 0:\n                    logger.debug(\"Controller exited with {0}\".format(x))\n                else:\n                    logger.error(\"Controller exited with {0}. May require manual cleanup\".format(x))\n            except subprocess.TimeoutExpired:\n                logger.warn(\"Ipcontroller process:{0} cleanup failed. May require manual cleanup\".format(self.proc.pid))\n\n        except Exception as e:\n            logger.warn(\"Failed to kill the ipcontroller process[{0}]: {1}\".format(self.proc.pid, e))\n", "path": "parsl/executors/ipp_controller.py"}]}