Repository: ansible-collections/community.general

Issue:
Some dconf invocations cause psutil.NoSuchProcess process no longer exists - possible race condition
### Summary

From time to time invocation of `dconf` ends up with a traceback. Error message starts with:
```
psutil.NoSuchProcess: psutil.NoSuchProcess no process found with pid 
```
followed by process ID that caused `psutil` to raise the exception.

### Issue Type

Bug Report

### Component Name

dconf

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.12.1]
  config file = /home/$USER/.ansible.cfg
  configured module search path = ['/home/$USER/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.10/site-packages/ansible
  ansible collection location = /home/$USER/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.10.2 (main, Jan 17 2022, 00:00:00) [GCC 11.2.1 20211203 (Red Hat 11.2.1-7)]
  jinja version = 3.0.1
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general
# /home/$USER/.ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.general 4.4.0  
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
DEFAULT_LOAD_CALLBACK_PLUGINS(/home/$USER/.ansible.cfg) = True
DEFAULT_STDOUT_CALLBACK(/home/$USER/.ansible.cfg) = yaml
```


### OS / Environment

```console
$ cat /etc/os-release 
NAME="Fedora Linux"
VERSION="35 (Cinnamon)"
ID=fedora
VERSION_ID=35
VERSION_CODENAME=""
PLATFORM_ID="platform:f35"
PRETTY_NAME="Fedora Linux 35 (Cinnamon)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:35"
HOME_URL="https://fedoraproject.org/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f35/system-administrators-guide/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://bugzilla.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=35
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=35
PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy"
VARIANT="Cinnamon"
VARIANT_ID=cinnamon
```

```console
 rpm -q python3-psutil
python3-psutil-5.8.0-12.fc35.x86_64
```

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- name: Set monospace font
  dconf:
    key: "/org/gnome/desktop/interface/monospace-font-name"
    value: "'Inconsolata Medium 13'"
```

This might need to be run couple of times as we are hoping for race condition.

Multiple `dconf` invocations might needed - I wasn't able to regularly reproduce this with just one.

### Expected Results

```
changed: [localhost]
```


### Actual Results

```console (paste below)
TASK [desktop_environment : Set monospace font] ********************************************************************************************************************************
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: psutil.NoSuchProcess: psutil.NoSuchProcess no process found with pid 59274
fatal: [ramona]: FAILED! => changed=false 
  module_stderr: |-
    Shared connection to ramona.m9.scrool.net closed.
  module_stdout: |-
    Traceback (most recent call last):
      File "/usr/lib64/python3.10/site-packages/psutil/_common.py", line 447, in wrapper
        ret = self._cache[fun]
    AttributeError: 'Process' object has no attribute '_cache'
  
    During handling of the above exception, another exception occurred:
  
    Traceback (most recent call last):
      File "/usr/lib64/python3.10/site-packages/psutil/_pslinux.py", line 1576, in wrapper
        return fun(self, *args, **kwargs)
      File "/usr/lib64/python3.10/site-packages/psutil/_common.py", line 450, in wrapper
        return fun(self)
      File "/usr/lib64/python3.10/site-packages/psutil/_pslinux.py", line 1618, in _parse_stat_file
        with open_binary("%s/%s/stat" % (self._procfs_path, self.pid)) as f:
      File "/usr/lib64/python3.10/site-packages/psutil/_common.py", line 711, in open_binary
        return open(fname, "rb", **kwargs)
    FileNotFoundError: [Errno 2] No such file or directory: '/proc/59274/stat'
  
    During handling of the above exception, another exception occurred:
  
    Traceback (most recent call last):
      File "/usr/lib64/python3.10/site-packages/psutil/__init__.py", line 354, in _init
        self.create_time()
      File "/usr/lib64/python3.10/site-packages/psutil/__init__.py", line 710, in create_time
        self._create_time = self._proc.create_time()
      File "/usr/lib64/python3.10/site-packages/psutil/_pslinux.py", line 1576, in wrapper
        return fun(self, *args, **kwargs)
      File "/usr/lib64/python3.10/site-packages/psutil/_pslinux.py", line 1788, in create_time
        ctime = float(self._parse_stat_file()['create_time'])
      File "/usr/lib64/python3.10/site-packages/psutil/_pslinux.py", line 1583, in wrapper
        raise NoSuchProcess(self.pid, self._name)
    psutil.NoSuchProcess: psutil.NoSuchProcess process no longer exists (pid=59274)
  
    During handling of the above exception, another exception occurred:
  
    Traceback (most recent call last):
      File "/var/tmp/ansible-tmp-1643906318.3007507-1131541-193823870965435/AnsiballZ_dconf.py", line 107, in <module>
        _ansiballz_main()
      File "/var/tmp/ansible-tmp-1643906318.3007507-1131541-193823870965435/AnsiballZ_dconf.py", line 99, in _ansiballz_main
        invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)
      File "/var/tmp/ansible-tmp-1643906318.3007507-1131541-193823870965435/AnsiballZ_dconf.py", line 47, in invoke_module
        runpy.run_module(mod_name='ansible_collections.community.general.plugins.modules.dconf', init_globals=dict(_module_fqn='ansible_collections.community.general.plugins.modules.dconf', _modlib_path=modlib_path),
      File "/usr/lib64/python3.10/runpy.py", line 209, in run_module
        return _run_module_code(code, init_globals, run_name, mod_spec)
      File "/usr/lib64/python3.10/runpy.py", line 96, in _run_module_code
        _run_code(code, mod_globals, init_globals,
      File "/usr/lib64/python3.10/runpy.py", line 86, in _run_code
        exec(code, run_globals)
      File "/tmp/ansible_dconf_payload_swqtyqzo/ansible_dconf_payload.zip/ansible_collections/community/general/plugins/modules/dconf.py", line 384, in <module>
      File "/tmp/ansible_dconf_payload_swqtyqzo/ansible_dconf_payload.zip/ansible_collections/community/general/plugins/modules/dconf.py", line 376, in main
      File "/tmp/ansible_dconf_payload_swqtyqzo/ansible_dconf_payload.zip/ansible_collections/community/general/plugins/modules/dconf.py", line 300, in write
      File "/tmp/ansible_dconf_payload_swqtyqzo/ansible_dconf_payload.zip/ansible_collections/community/general/plugins/modules/dconf.py", line 160, in __init__
      File "/tmp/ansible_dconf_payload_swqtyqzo/ansible_dconf_payload.zip/ansible_collections/community/general/plugins/modules/dconf.py", line 183, in _get_existing_dbus_session
      File "/usr/lib64/python3.10/site-packages/psutil/__init__.py", line 326, in __init__
        self._init(pid)
      File "/usr/lib64/python3.10/site-packages/psutil/__init__.py", line 367, in _init
        raise NoSuchProcess(pid, None, msg)
    psutil.NoSuchProcess: psutil.NoSuchProcess no process found with pid 59274
  msg: |-
    MODULE FAILURE
    See stdout/stderr for the exact error
  rc: 1
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/system/dconf.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/system/dconf.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @azaghal
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Relevant method from `dconf.py`:

```python
    def _get_existing_dbus_session(self):
        """
        Detects and returns an existing D-Bus session bus address.

        :returns: string -- D-Bus session bus address. If a running D-Bus session was not detected, returns None.
        """

        # We'll be checking the processes of current user only.
        uid = os.getuid()

        # Go through all the pids for this user, try to extract the D-Bus
        # session bus address from environment, and ensure it is possible to
        # connect to it.
        self.module.debug("Trying to detect existing D-Bus user session for user: %d" % uid)

        for pid in psutil.pids():
            process = psutil.Process(pid)
            process_real_uid, dummy, dummy = process.uids()
            try:
                if process_real_uid == uid and 'DBUS_SESSION_BUS_ADDRESS' in process.environ():
                    dbus_session_bus_address_candidate = process.environ()['DBUS_SESSION_BUS_ADDRESS']
                    self.module.debug("Found D-Bus user session candidate at address: %s" % dbus_session_bus_address_candidate)
                    dbus_send_cmd = self.module.get_bin_path('dbus-send', required=True)
                    command = [dbus_send_cmd, '--address=%s' % dbus_session_bus_address_candidate, '--type=signal', '/', 'com.example.test']
                    rc, dummy, dummy = self.module.run_command(command)

                    if rc == 0:
                        self.module.debug("Verified D-Bus user session candidate as usable at address: %s" % dbus_session_bus_address_candidate)

                        return dbus_session_bus_address_candidate

            # This can happen with things like SSH sessions etc.
            except psutil.AccessDenied:
                pass

        self.module.debug("Failed to find running D-Bus user session, will use dbus-run-session")

        return None
```

where from my experience either of:
```python
            process = psutil.Process(pid)
            process_real_uid, dummy, dummy = process.uids()
```
could raise `psutil.NoSuchProcess`. I can think of three approaches:

- wrap these two lines like this:
  ```python
            try:
                process = psutil.Process(pid)
                process_real_uid, dummy, dummy = process.uids()
            except psutil.NoSuchProcess:
                continue
- extend `except psutil.AccessDenied` later in the code to catch also `psutil.NoSuchProcess`:
  ```python
            try:
                process = psutil.Process(pid)
                process_real_uid, dummy, dummy = process.uids()

            # ... (snip)

            # This can happen with things like SSH sessions etc.
            except (psutil.AccessDenied, psutil.NoSuchProcess):
                pass
- use separate except for easier documentation
  ```python
            try:
                process = psutil.Process(pid)
                process_real_uid, dummy, dummy = process.uids()

            # ... (snip)

            # This can happen with things like SSH sessions etc.
            except psutil.AccessDenied:
                pass
            # Process no longer exists
            except psutil.NoSuchProcess:
                pass
   ```
Ah, that's a very nice catch, thanks for also digging into the code and coming up with the solutions. Looking now at the code that follows those two lines, I can see more spots where a similar error could be thrown - essentially anything that utilises the `process` object (like `process.environ()`, for example).

I think the last option you presented would probably be the best one - for documentation purposes, as you mentioned.

I am now also kinda wondering what would happen if the located D-Bus process would suddenly disappear as well, but that's something that would need to cause hard fail one way or another - most likely producing a nicer-looking error message would be beneficial.

Want to try to create a small PR yourself or...? :) My only input would be maybe to change the comment to "Process disappeared while inspecting it" or something, but that's really nitpicking :)

Resolving patch:
diff --git a/plugins/modules/system/dconf.py b/plugins/modules/system/dconf.py
--- a/plugins/modules/system/dconf.py
+++ b/plugins/modules/system/dconf.py
@@ -180,9 +180,9 @@ def _get_existing_dbus_session(self):
         self.module.debug("Trying to detect existing D-Bus user session for user: %d" % uid)
 
         for pid in psutil.pids():
-            process = psutil.Process(pid)
-            process_real_uid, dummy, dummy = process.uids()
             try:
+                process = psutil.Process(pid)
+                process_real_uid, dummy, dummy = process.uids()
                 if process_real_uid == uid and 'DBUS_SESSION_BUS_ADDRESS' in process.environ():
                     dbus_session_bus_address_candidate = process.environ()['DBUS_SESSION_BUS_ADDRESS']
                     self.module.debug("Found D-Bus user session candidate at address: %s" % dbus_session_bus_address_candidate)
@@ -198,6 +198,9 @@ def _get_existing_dbus_session(self):
             # This can happen with things like SSH sessions etc.
             except psutil.AccessDenied:
                 pass
+            # Process has disappeared while inspecting it
+            except psutil.NoSuchProcess:
+                pass
 
         self.module.debug("Failed to find running D-Bus user session, will use dbus-run-session")
 


Test patch: