Repository: ultrabug/py3status

Issue:
mpd_status creates a new socket connection every second
I'm not sure if this should be a bug report or a feature request, but I've been bitten by this, erm, _feature_: I use `mopidy` (mpd with spotify integration) and that gets confused when a request comes in from `mpd_status` while it processes a mpd-command-block. 

This also keeps a lot of abandoned connections in `TIME_WAIT` state - 60 currently at my system. 

I've had a quick look at the code for this module, and the connection i s esatablished in `current_track()`. I do not know if it is possible, but the best apporach (IMHO) would be to `connect()` once and keep the socket alive until `py3status` gets reloaded. 
mpd_status creates a new socket connection every second
I'm not sure if this should be a bug report or a feature request, but I've been bitten by this, erm, _feature_: I use `mopidy` (mpd with spotify integration) and that gets confused when a request comes in from `mpd_status` while it processes a mpd-command-block. 

This also keeps a lot of abandoned connections in `TIME_WAIT` state - 60 currently at my system. 

I've had a quick look at the code for this module, and the connection i s esatablished in `current_track()`. I do not know if it is possible, but the best apporach (IMHO) would be to `connect()` once and keep the socket alive until `py3status` gets reloaded. 


Discussion:
@girst it's not a module I use, but if you have a patch that fixes this that would be great
my python is very rusty. I've made some progress, but then broke it again somehow. do you have any idea why i could get the following error (when launching the `mpd_status.py` manually)?

```
response should be a dict (TypeError) module.py line 763.
Traceback
TypeError: response should be a dict
  File "/usr/lib/python3.6/site-packages/py3status/module.py", line 763, in run
    raise TypeError('response should be a dict')
```

I've found no way to attach a patch, so there it is:

```
diff --git a/py3status/modules/mpd_status.py b/py3status/modules/mpd_status.py
index 65af95b..264f4c1 100644
--- a/py3status/modules/mpd_status.py
+++ b/py3status/modules/mpd_status.py
@@ -110,6 +110,20 @@ class Py3status:
     state_play = '[play]'
     state_stop = '[stop]'
 
+    c = None
+
+    def mpdc(self):
+        if self.c is None:
+            self.c = MPDClient()
+            self.c.connect(host=self.host, port=self.port)
+            if self.password:
+                self.c.password(self.password)
+        return self.c
+
+    def mpd_disconnect(self):
+        self.mpdc().disconnect()
+        self.c = None
+
     def post_config_hook(self):
         # Convert from %placeholder% to {placeholder}
         # This is not perfect but should be good enough
@@ -128,12 +142,7 @@ class Py3status:
 
     def current_track(self):
         try:
-            c = MPDClient()
-            c.connect(host=self.host, port=self.port)
-            if self.password:
-                c.password(self.password)
-
-            status = c.status()
+            status = self.mpdc().status()
             song = int(status.get('song', 0))
             next_song = int(status.get('nextsong', 0))
 
@@ -144,7 +153,7 @@ class Py3status:
                 text = ''
 
             else:
-                playlist_info = c.playlistinfo()
+                playlist_info = self.mpdc().playlistinfo()
                 try:
                     song = playlist_info[song]
                 except IndexError:
@@ -170,9 +179,10 @@ class Py3status:
         except CommandError:
             text = "Failed to authenticate to mpd!"
             state = None
-            c.disconnect()
-        else:
-            c.disconnect()
+            #c.disconnect()
+            self.mpd_disconnect()
+        ##else:
+            #c.disconnect()
 
         if len(text) > self.max_width:
             text = u'{}...'.format(text[:self.max_width - 3])

```
```
     def current_track(self):
```
~~... it should be `def mpd(self):`...~~
... it should be `def mpd_status(self):`... 
```
+    def mpd_disconnect(self):
+    def mpdc(self):
```
... it should be `def _mpd_disconnect(self):`... 
... it should be `def _mpdc(self):`... 

We allow one method for the loop... and that method can be named anything... In this case, `current_track` is being used here. Other methods will need to start with `_` except for few special methods: `post_config_hook`, `kill`, `on_click`.

I think ideally, we should name the loop method after the module... is which why I said... it should be ~~`def mpd(self):`~~ `def mpd_status(self):` above... then just add the missing underscore for the other methods. Scroll down to find special methods. http://py3status.readthedocs.io/en/3.6/writing_modules.html#module-methods

so, it's 5:13 here. I've fixed the underscore issue (the hell!?!). I have __NOT__ done any testing yet, but I'll post my patch, since I don't  want to lose it. 

when run either directly or with `python3 -m py3status.__init__` it opens one connection and keeps using that one (assuming mpd/mopidy/etc is already running -- again, no testing yet)

```
diff --git a/py3status/modules/mpd_status.py b/py3status/modules/mpd_status.py
index 65af95b..d479521 100644
--- a/py3status/modules/mpd_status.py
+++ b/py3status/modules/mpd_status.py
@@ -110,6 +110,20 @@ class Py3status:
     state_play = '[play]'
     state_stop = '[stop]'
 
+    c = None
+
+    def _mpdc(self):
+        if self.c is None:
+            self.c = MPDClient()
+            self.c.connect(host=self.host, port=self.port)
+            if self.password:
+                self.c.password(self.password)
+        return self.c
+
+    def _mpd_disconnect(self):
+        self._mpdc().disconnect()
+        self.c = None
+
     def post_config_hook(self):
         # Convert from %placeholder% to {placeholder}
         # This is not perfect but should be good enough
@@ -128,12 +142,7 @@ class Py3status:
 
     def current_track(self):
         try:
-            c = MPDClient()
-            c.connect(host=self.host, port=self.port)
-            if self.password:
-                c.password(self.password)
-
-            status = c.status()
+            status = self._mpdc().status()
             song = int(status.get('song', 0))
             next_song = int(status.get('nextsong', 0))
 
@@ -144,7 +153,7 @@ class Py3status:
                 text = ''
 
             else:
-                playlist_info = c.playlistinfo()
+                playlist_info = self._mpdc().playlistinfo()
                 try:
                     song = playlist_info[song]
                 except IndexError:
@@ -170,9 +179,7 @@ class Py3status:
         except CommandError:
             text = "Failed to authenticate to mpd!"
             state = None
-            c.disconnect()
-        else:
-            c.disconnect()
+            self._mpd_disconnect()
 
         if len(text) > self.max_width:
             text = u'{}...'.format(text[:self.max_width - 3])
```
`def kill()` can be useful for disconnecting socket too. My computer is dirty. Can't initialize `mpd` without getting errors, assertions, etc. I could go through #856 again when #1005 get merged... along with your socket patch.
@girst it's not a module I use, but if you have a patch that fixes this that would be great
my python is very rusty. I've made some progress, but then broke it again somehow. do you have any idea why i could get the following error (when launching the `mpd_status.py` manually)?

```
response should be a dict (TypeError) module.py line 763.
Traceback
TypeError: response should be a dict
  File "/usr/lib/python3.6/site-packages/py3status/module.py", line 763, in run
    raise TypeError('response should be a dict')
```

I've found no way to attach a patch, so there it is:

```
diff --git a/py3status/modules/mpd_status.py b/py3status/modules/mpd_status.py
index 65af95b..264f4c1 100644
--- a/py3status/modules/mpd_status.py
+++ b/py3status/modules/mpd_status.py
@@ -110,6 +110,20 @@ class Py3status:
     state_play = '[play]'
     state_stop = '[stop]'
 
+    c = None
+
+    def mpdc(self):
+        if self.c is None:
+            self.c = MPDClient()
+            self.c.connect(host=self.host, port=self.port)
+            if self.password:
+                self.c.password(self.password)
+        return self.c
+
+    def mpd_disconnect(self):
+        self.mpdc().disconnect()
+        self.c = None
+
     def post_config_hook(self):
         # Convert from %placeholder% to {placeholder}
         # This is not perfect but should be good enough
@@ -128,12 +142,7 @@ class Py3status:
 
     def current_track(self):
         try:
-            c = MPDClient()
-            c.connect(host=self.host, port=self.port)
-            if self.password:
-                c.password(self.password)
-
-            status = c.status()
+            status = self.mpdc().status()
             song = int(status.get('song', 0))
             next_song = int(status.get('nextsong', 0))
 
@@ -144,7 +153,7 @@ class Py3status:
                 text = ''
 
             else:
-                playlist_info = c.playlistinfo()
+                playlist_info = self.mpdc().playlistinfo()
                 try:
                     song = playlist_info[song]
                 except IndexError:
@@ -170,9 +179,10 @@ class Py3status:
         except CommandError:
             text = "Failed to authenticate to mpd!"
             state = None
-            c.disconnect()
-        else:
-            c.disconnect()
+            #c.disconnect()
+            self.mpd_disconnect()
+        ##else:
+            #c.disconnect()
 
         if len(text) > self.max_width:
             text = u'{}...'.format(text[:self.max_width - 3])

```
```
     def current_track(self):
```
~~... it should be `def mpd(self):`...~~
... it should be `def mpd_status(self):`... 
```
+    def mpd_disconnect(self):
+    def mpdc(self):
```
... it should be `def _mpd_disconnect(self):`... 
... it should be `def _mpdc(self):`... 

We allow one method for the loop... and that method can be named anything... In this case, `current_track` is being used here. Other methods will need to start with `_` except for few special methods: `post_config_hook`, `kill`, `on_click`.

I think ideally, we should name the loop method after the module... is which why I said... it should be ~~`def mpd(self):`~~ `def mpd_status(self):` above... then just add the missing underscore for the other methods. Scroll down to find special methods. http://py3status.readthedocs.io/en/3.6/writing_modules.html#module-methods

so, it's 5:13 here. I've fixed the underscore issue (the hell!?!). I have __NOT__ done any testing yet, but I'll post my patch, since I don't  want to lose it. 

when run either directly or with `python3 -m py3status.__init__` it opens one connection and keeps using that one (assuming mpd/mopidy/etc is already running -- again, no testing yet)

```
diff --git a/py3status/modules/mpd_status.py b/py3status/modules/mpd_status.py
index 65af95b..d479521 100644
--- a/py3status/modules/mpd_status.py
+++ b/py3status/modules/mpd_status.py
@@ -110,6 +110,20 @@ class Py3status:
     state_play = '[play]'
     state_stop = '[stop]'
 
+    c = None
+
+    def _mpdc(self):
+        if self.c is None:
+            self.c = MPDClient()
+            self.c.connect(host=self.host, port=self.port)
+            if self.password:
+                self.c.password(self.password)
+        return self.c
+
+    def _mpd_disconnect(self):
+        self._mpdc().disconnect()
+        self.c = None
+
     def post_config_hook(self):
         # Convert from %placeholder% to {placeholder}
         # This is not perfect but should be good enough
@@ -128,12 +142,7 @@ class Py3status:
 
     def current_track(self):
         try:
-            c = MPDClient()
-            c.connect(host=self.host, port=self.port)
-            if self.password:
-                c.password(self.password)
-
-            status = c.status()
+            status = self._mpdc().status()
             song = int(status.get('song', 0))
             next_song = int(status.get('nextsong', 0))
 
@@ -144,7 +153,7 @@ class Py3status:
                 text = ''
 
             else:
-                playlist_info = c.playlistinfo()
+                playlist_info = self._mpdc().playlistinfo()
                 try:
                     song = playlist_info[song]
                 except IndexError:
@@ -170,9 +179,7 @@ class Py3status:
         except CommandError:
             text = "Failed to authenticate to mpd!"
             state = None
-            c.disconnect()
-        else:
-            c.disconnect()
+            self._mpd_disconnect()
 
         if len(text) > self.max_width:
             text = u'{}...'.format(text[:self.max_width - 3])
```
`def kill()` can be useful for disconnecting socket too. My computer is dirty. Can't initialize `mpd` without getting errors, assertions, etc. I could go through #856 again when #1005 get merged... along with your socket patch.

Resolving patch:
diff --git a/py3status/modules/mpd_status.py b/py3status/modules/mpd_status.py
--- a/py3status/modules/mpd_status.py
+++ b/py3status/modules/mpd_status.py
@@ -65,7 +65,7 @@
 import datetime
 import re
 import socket
-from mpd import MPDClient, CommandError
+from mpd import MPDClient, CommandError, ConnectionError
 
 
 def song_attr(song, attr):
@@ -116,6 +116,27 @@ def post_config_hook(self):
         if not self.py3.get_placeholders_list(self.format) and '%' in self.format:
             self.format = re.sub('%([a-z]+)%', r'{\1}', self.format)
             self.py3.log('Old % style format DEPRECATED use { style format')
+        # class variables:
+        self.client = None
+
+    def _get_mpd(self, disconnect=False):
+        if disconnect:
+            try:
+                self.client.disconnect()
+            finally:
+                self.client = None
+            return
+
+        try:
+            if self.client is None:
+                self.client = MPDClient()
+                self.client.connect(host=self.host, port=self.port)
+                if self.password:
+                    self.client.password(self.password)
+            return self.client
+        except (socket.error, ConnectionError, CommandError) as e:
+            self.client = None
+            raise e
 
     def _state_character(self, state):
         if state == 'play':
@@ -128,12 +149,7 @@ def _state_character(self, state):
 
     def current_track(self):
         try:
-            c = MPDClient()
-            c.connect(host=self.host, port=self.port)
-            if self.password:
-                c.password(self.password)
-
-            status = c.status()
+            status = self._get_mpd().status()
             song = int(status.get('song', 0))
             next_song = int(status.get('nextsong', 0))
 
@@ -144,7 +160,7 @@ def current_track(self):
                 text = ''
 
             else:
-                playlist_info = c.playlistinfo()
+                playlist_info = self._get_mpd().playlistinfo()
                 try:
                     song = playlist_info[song]
                 except IndexError:
@@ -167,12 +183,14 @@ def attr_getter(attr):
         except socket.error:
             text = "Failed to connect to mpd!"
             state = None
+        except ConnectionError:
+            text = "Error while connecting to mpd!"
+            state = None
+            self._get_mpd(disconnect=True)
         except CommandError:
             text = "Failed to authenticate to mpd!"
             state = None
-            c.disconnect()
-        else:
-            c.disconnect()
+            self._get_mpd(disconnect=True)
 
         if len(text) > self.max_width:
             text = u'{}...'.format(text[:self.max_width - 3])
@@ -193,6 +211,9 @@ def attr_getter(attr):
 
         return response
 
+    def kill(self):
+        self._get_mpd(disconnect=True)
+
 
 if __name__ == "__main__":
     """


Test patch: