Repository: ros/ros_comm

Issue:
Roslaunch Creates Unexpected Private Parameters
I came across a bug on the implementation of roslaunch. I have tested it both in Indigo and Kinetic, and it is also present in `lunar-devel` from what I can see.

Consider the following example launch,

```xml
<launch>
  <node name="driver" pkg="drivers" type="driver">
    <param name="param1" value="1" />
    <param name="~param2" value="2" />
  </node>
  <node name="controller" pkg="controllers" type="controller" />
</launch>
```

As you would expect, roslaunch creates two parameters:

  - `/driver/param1`
  - `/driver/param2`

Consider yet another simple example.

```xml
<launch>
  <param name="~param" value="1" />
  <node name="driver" pkg="drivers" type="driver" />
  <node name="controller" pkg="controllers" type="controller" />
</launch>
```

Again, two parameters, as expected.

  - `/driver/param`
  - `/controller/param`

If we combine both features, the second node inherits the first node's private parameters, when it should not.

```xml
<launch>
  <param name="~param1" value="1" />
  <node name="driver" pkg="drivers" type="driver">
    <param name="param2" value="2" />
    <param name="~param3" value="3" />
  </node>
  <node name="controller" pkg="controllers" type="controller" />
</launch>
```

This results in ***six*** parameters, instead of four.

  - `/driver/param1`
  - `/driver/param2`
  - `/driver/param3`
  - `/controller/param1`
  - `/controller/param2`
  - `/controller/param3`

I have tried variations of isolating either the first or second node in `<group>`, but the result is the same.

From my understanding of the code, the problem lies in [line 172 of `loader.py`](https://github.com/ros/ros_comm/blob/44bbbd1a7c383f1b1901c024010992ad7b17b0c6/tools/roslaunch/src/roslaunch/loader.py#L172) and [line 320](https://github.com/ros/ros_comm/blob/44bbbd1a7c383f1b1901c024010992ad7b17b0c6/tools/roslaunch/src/roslaunch/loader.py#L320) of the same file.

When parsing the first private parameter, the parameter is [added to the root context](https://github.com/ros/ros_comm/blob/44bbbd1a7c383f1b1901c024010992ad7b17b0c6/tools/roslaunch/src/roslaunch/xmlloader.py#L270).

```python
if is_private(name) or force_local:
    p = Param(name, value)
    context.add_param(p)
```

When parsing the node tag right after, a child context is created, and this is where the problem lies. The child context does not start with its own list of parameters, it simply [reuses the same list](https://github.com/ros/ros_comm/blob/44bbbd1a7c383f1b1901c024010992ad7b17b0c6/tools/roslaunch/src/roslaunch/loader.py#L320), over and over.

```python
def child(self, ns):
    ...
    return LoaderContext(..., params=self.params, ...)
```

The result is that `param1` is added to the list, and `param2` and `param3` are added to the same list as well, because the child context is using the same reference as its parent context. When parsing the second (third, fourth, ...) `<node>`, it will create not only `param1`, but the other parameters as well, since they were wrongly appended to the root list.

Why does this work as intended if the first parameter is not present? It works because the list is empty when the first `<node>` is parsed. When creating the child context, an empty list (the root list) is passed to the `LoaderContext` constructor. As seen in [line 172](https://github.com/ros/ros_comm/blob/44bbbd1a7c383f1b1901c024010992ad7b17b0c6/tools/roslaunch/src/roslaunch/loader.py#L172), if the received list is empty, it does not pass the logical `or`, and a **new** parameter list is created. Since a new list is created, it isolates the parameters properly.

```python
self.params = params or []
```

I have a few ideas to solve this issue, but I am not sure of the impact of each, and what is the intended behaviour. If one of the following is the clear solution, I can submit a pull request.

  1. Change `LoaderContext` constructor. Simply create a new empty list for `self.params` every time.
  2. Change the `child` method to pass down either an empty list or a *copy* of the current list.
  3. Change `XmlLoader` to overwrite `param_ns.params` with an empty list ([l. 372](https://github.com/ros/ros_comm/blob/44bbbd1a7c383f1b1901c024010992ad7b17b0c6/tools/roslaunch/src/roslaunch/xmlloader.py#L372)).


Discussion:
Thanks for the detailed report! Looks like this is an issue with the tilde param functionality, since the following:

```
<launch>
  <param name="param1" value="1" />
  <node name="driver" pkg="drivers" type="driver">
    <param name="param2" value="2" />
    <param name="param3" value="3" />
  </node>
  <node name="controller" pkg="controllers" type="controller" />
</launch>
```

Gives the expected result:

```
$ roslaunch test.launch --dump-params
{/driver/param2: 2, /driver/param3: 3, /param1: 1}
```

The [documentation](http://wiki.ros.org/roslaunch/XML/param) says:

> You can also set private parameter across a group of nodes by using the ~param syntax (see ROS names) in a <param> tag. The declared parameter will be set as a local parameter in the <node> tags that follow that are in the same scope (i.e. group or ns tag).

So I agree that this behaviour is absolutely a bug.

Resolving patch:
diff --git a/tools/roslaunch/src/roslaunch/xmlloader.py b/tools/roslaunch/src/roslaunch/xmlloader.py
--- a/tools/roslaunch/src/roslaunch/xmlloader.py
+++ b/tools/roslaunch/src/roslaunch/xmlloader.py
@@ -370,6 +370,7 @@ def _node_tag(self, tag, context, ros_config, default_machine, is_test=False, ve
                     
             child_ns = self._ns_clear_params_attr('node', tag, context, ros_config, node_name=name)
             param_ns = child_ns.child(name)
+            param_ns.params = [] # This is necessary because child() does not make a copy of the param list.
                 
             # required attributes
             pkg, node_type = self.reqd_attrs(tag, context, ('pkg', 'type'))
@@ -649,6 +650,7 @@ def _recurse_load(self, ros_config, tags, context, default_machine, is_core, ver
                 if ifunless_test(self, tag, context):
                     self._check_attrs(tag, context, ros_config, XmlLoader.GROUP_ATTRS)
                     child_ns = self._ns_clear_params_attr(name, tag, context, ros_config)
+                    child_ns.params = list(child_ns.params) # copy is needed here to enclose new params
                     default_machine = \
                         self._recurse_load(ros_config, tag.childNodes, child_ns, \
                                                default_machine, is_core, verbose)


Test patch:
diff --git a/tools/roslaunch/test/unit/test_roslaunch_dump_params.py b/tools/roslaunch/test/unit/test_roslaunch_dump_params.py
--- a/tools/roslaunch/test/unit/test_roslaunch_dump_params.py
+++ b/tools/roslaunch/test/unit/test_roslaunch_dump_params.py
@@ -77,6 +77,10 @@ def test_roslaunch(self):
             '/node_rosparam/dict1/shoulders': 2,
             '/node_rosparam/dict1/knees': 3,
             '/node_rosparam/dict1/toes': 4,
+            '/node_rosparam/tilde1': 'foo',
+            '/node_rosparam/local_param': 'baz',
+
+            '/node_rosparam2/tilde1': 'foo',
 
             '/inline_str': 'value1',
             '/inline_list': [1, 2, 3, 4],
@@ -99,3 +103,6 @@ def test_roslaunch(self):
                 elif v != output_val[k]:
                     self.fail("key [%s] value [%s] does not match output: %s"%(k, v, output_val[k])) 
         self.assertEquals(val, output_val)
+        for k in ('/node_rosparam/tilde2', '/node_rosparam2/tilde2', '/node_rosparam2/local_param'):
+            if k in output_val:
+                self.fail("key [%s] should not be in output: %s"%(k, output_val))
diff --git a/tools/roslaunch/test/xml/test-dump-rosparam.launch b/tools/roslaunch/test/xml/test-dump-rosparam.launch
--- a/tools/roslaunch/test/xml/test-dump-rosparam.launch
+++ b/tools/roslaunch/test/xml/test-dump-rosparam.launch
@@ -1,15 +1,21 @@
 <launch>
 
+  <param name="~tilde1" value="foo" />
   <rosparam file="$(find roslaunch)/test/dump-params.yaml" command="load" />
   
   <group ns="rosparam">
+    <param name="~tilde2" value="bar" />
     <rosparam file="$(find roslaunch)/test/dump-params.yaml" command="load" />
   </group>
 
   <node pkg="package" type="test_base" name="node_rosparam">
+    <param name="local_param" value="baz" />
     <rosparam file="$(find roslaunch)/test/dump-params.yaml" command="load" />      
   </node>
 
+  <node pkg="package" type="test_base" name="node_rosparam2">
+  </node>
+
   <rosparam param="inline_str">value1</rosparam>
   <rosparam param="inline_list">[1, 2, 3, 4]</rosparam>  
   <rosparam param="inline_dict">{key1: value1, key2: value2}</rosparam>