Repository: plone/Products.CMFPlone

Issue:
overview-controlpanel view do not work with gunicorn 
## BUG/PROBLEM REPORT (OR OTHER COMMON ISSUE)
overview-controlpanel view do not work with gunicorn. This view get server_name from `SERVER_SOFTWARE` environment variable and try to parse this name to get version from pkg_resources. (https://github.com/plone/Products.CMFPlone/blob/master/Products/CMFPlone/controlpanel/browser/overview.py#L55)

As I can see in Gunicorn, `SERVER_SOFTWARE` variable is equal to "gunicorn/19.9.0"  (https://github.com/benoitc/gunicorn/blob/19.x/gunicorn/__init__.py#L8) which is not parsable for pkg_resources.

<!--

Read https://plone.org/support/bugs first!

Please use the labels at Github, at least one of the types: bug, regression, question, enhancement.

Please include tracebacks, screenshots, code of debugging sessions or code that reproduces the issue if possible.
The best reproductions are in plain Plone installations without addons or at least with minimal needed addons installed.

-->

### What I did:

<!-- Please a reproducable description, including preconditions. -->
I start buildout coredev with  wsgioptions.cfg config file and start gunicorn-instance like that:
```
./bin/buildout -c wsgioptions.cfg
./bin/gunicorn-instance
```
then create a Plone site and go to http://localhost:8080/Plone/overview-controlpanel

### What I expect to happen:
overview-controlpanel view with all information about server (gunicorn version, plone version, python version, ...)

### What actually happened:
a template error 
```
Traceback (innermost last):
  Module ZPublisher.WSGIPublisher, line 167, in transaction_pubevents
  Module ZPublisher.WSGIPublisher, line 376, in publish_module
  Module ZPublisher.WSGIPublisher, line 271, in publish
  Module ZPublisher.mapply, line 85, in mapply
  Module ZPublisher.WSGIPublisher, line 68, in call_object
  Module Products.CMFPlone.controlpanel.browser.overview, line 34, in __call__
  Module Products.Five.browser.pagetemplatefile, line 126, in __call__
  Module Products.Five.browser.pagetemplatefile, line 58, in __call__
  Module zope.pagetemplate.pagetemplate, line 133, in pt_render
  Module Products.PageTemplates.engine, line 365, in __call__
  Module z3c.pt.pagetemplate, line 176, in render
  Module chameleon.zpt.template, line 302, in render
  Module chameleon.template, line 215, in render
  Module chameleon.utils, line 53, in raise_with_traceback
  Module chameleon.template, line 192, in render
  Module 73c1d855834a9decac0c0da98ce48be5, line 1255, in render
  Module 4a6b6042c5725aa6b3a089c76a55dfe1, line 913, in render_master
  Module a990005b16c5c23df8d22e18c41fcf46, line 925, in render_master
  Module 4a6b6042c5725aa6b3a089c76a55dfe1, line 888, in __fill_content
  Module a990005b16c5c23df8d22e18c41fcf46, line 1581, in render_content
  Module 4a6b6042c5725aa6b3a089c76a55dfe1, line 874, in __fill_main
  Module 73c1d855834a9decac0c0da98ce48be5, line 1000, in __fill_prefs_configlet_main
  Module zope.tales.expressions, line 250, in __call__
  Module Products.PageTemplates.Expressions, line 221, in _eval
  Module Products.PageTemplates.Expressions, line 152, in render
  Module Products.CMFPlone.controlpanel.browser.overview, line 61, in server_info
  Module pkg_resources, line 469, in get_distribution
  Module pkg_resources, line 3154, in parse
  Module pkg_resources, line 3099, in parse_requirements
  Module pkg_resources, line 3109, in __init__
  Module pkg_resources.extern.packaging.requirements, line 104, in __init__
pkg_resources.extern.packaging.requirements.InvalidRequirement: pkg_resources.extern.packaging.requirements.InvalidRequirement: Parse error at "'/19.9.0'": Expected stringEnd
```

### What version of Plone/ Addons I am using:
6.0.0a3


Discussion:

Resolving patch:
diff --git a/Products/CMFPlone/controlpanel/browser/overview.py b/Products/CMFPlone/controlpanel/browser/overview.py
--- a/Products/CMFPlone/controlpanel/browser/overview.py
+++ b/Products/CMFPlone/controlpanel/browser/overview.py
@@ -15,6 +15,7 @@
 
 try:
     import plone.app.event
+
     plone.app.event  # pyflakes
     HAS_PAE = True
 except ImportError:
@@ -23,70 +24,76 @@
 
 class OverviewControlPanel(controlpanel.RegistryEditForm):
 
-    template = ViewPageTemplateFile('overview.pt')
+    template = ViewPageTemplateFile("overview.pt")
 
-    base_category = 'controlpanel'
-    ignored_categories = ('controlpanel_user')
+    base_category = "controlpanel"
+    ignored_categories = "controlpanel_user"
 
     def __call__(self):
-        self.request.set('disable_border', 1)
+        self.request.set("disable_border", 1)
         return self.template()
 
     @memoize
     def cptool(self):
-        return getToolByName(aq_inner(self.context), 'portal_controlpanel')
+        return getToolByName(aq_inner(self.context), "portal_controlpanel")
 
     @memoize
     def migration(self):
-        return getToolByName(aq_inner(self.context), 'portal_migration')
+        return getToolByName(aq_inner(self.context), "portal_migration")
 
     @memoize
     def core_versions(self):
         return self.migration().coreVersions()
 
     def pil(self):
-        return 'PIL' in self.core_versions()
+        return "PIL" in self.core_versions()
 
     def server_info(self):
         wsgi = isinstance(self.request, WSGIRequest)
-        server_name = 'unknown'
-        server_version = ''
+        server_name = "unknown"
+        server_version = ""
 
-        server_name = self.request.get('SERVER_SOFTWARE')
+        server_name = self.request.get("SERVER_SOFTWARE")
         if server_name:
-            if 'ZServer' in server_name:
-                server_name = 'ZServer'
+            if "ZServer" in server_name:
+                server_name = "ZServer"
+            elif "/" in server_name:
+                server_name = server_name.split("/")[0]
             try:
                 server = pkg_resources.get_distribution(server_name)
                 server_version = server.version
-            except (pkg_resources.DistributionNotFound,
-                    pkg_resources.RequirementParseError):
+            except (
+                pkg_resources.DistributionNotFound,
+                pkg_resources.RequirementParseError,
+            ):
                 pass
         return {
-            'wsgi': wsgi,
-            'server_name': server_name,
-            'version': server_version,
+            "wsgi": wsgi,
+            "server_name": server_name,
+            "version": server_version,
         }
 
     def version_overview(self):
 
         core_versions = self.core_versions()
         versions = [
-            'Plone {} ({})'.format(core_versions['Plone'],
-                               core_versions['Plone Instance'])]
-
-        for v in ('CMF', 'Zope', 'Python'):
-            versions.append(v + ' ' + core_versions.get(v))
-        pil = core_versions.get('PIL', None)
+            "Plone {} ({})".format(
+                core_versions["Plone"], core_versions["Plone Instance"]
+            )
+        ]
+
+        for v in ("CMF", "Zope", "Python"):
+            versions.append(v + " " + core_versions.get(v))
+        pil = core_versions.get("PIL", None)
         if pil is not None:
-            versions.append('PIL ' + pil)
+            versions.append("PIL " + pil)
         return versions
 
     def is_dev_mode(self):
         return getConfiguration().debug_mode
 
     def upgrade_warning(self):
-        mt = getToolByName(aq_inner(self.context), 'portal_migration')
+        mt = getToolByName(aq_inner(self.context), "portal_migration")
         if mt.needUpgrading():
             # if the user can't run the upgrade, no sense in displaying the
             # message
@@ -97,8 +104,7 @@ def upgrade_warning(self):
 
     def mailhost_warning(self):
         registry = getUtility(IRegistry)
-        mail_settings = registry.forInterface(
-            IMailSchema, prefix='plone', check=False)
+        mail_settings = registry.forInterface(IMailSchema, prefix="plone", check=False)
         mailhost = mail_settings.smtp_host
         email = mail_settings.email_from_address
         if mailhost and email:
@@ -106,8 +112,7 @@ def mailhost_warning(self):
         return True
 
     def timezone_warning(self):
-        """Returns true, if the portal_timezone is not set in the registry.
-        """
+        """Returns true, if the portal_timezone is not set in the registry."""
         if not HAS_PAE:
             # No point of having a portal timezone configured without
             # plone.app.event installed.
@@ -121,7 +126,7 @@ def timezone_warning(self):
         if reg_key not in registry:
             # else use 'plone.app.event.portal_timezone'
             # < Plone 5
-            reg_key = 'plone.app.event.portal_timezone'
+            reg_key = "plone.app.event.portal_timezone"
         if reg_key not in registry:
             return True
         portal_timezone = registry[reg_key]


Test patch:
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_overview.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_overview.py
--- a/Products/CMFPlone/controlpanel/tests/test_controlpanel_overview.py
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_overview.py
@@ -4,6 +4,7 @@
 from plone.registry.interfaces import IRegistry
 from zope.component import getUtility
 from unittest import mock
+import os
 import unittest
 
 
@@ -12,15 +13,15 @@ def mock_getUtility1(iface):
 
 
 def mock_getUtility2(iface):
-    return {'plone.portal_timezone': None}
+    return {"plone.portal_timezone": None}
 
 
 def mock_getUtility3(iface):
-    return {'plone.portal_timezone': 'Europe/Amsterdam'}
+    return {"plone.portal_timezone": "Europe/Amsterdam"}
 
 
 def mock_getUtility4(iface):
-    return {'plone.app.event.portal_timezone': 'Europe/Amsterdam'}
+    return {"plone.app.event.portal_timezone": "Europe/Amsterdam"}
 
 
 class TestControlPanel(unittest.TestCase):
@@ -28,34 +29,45 @@ class TestControlPanel(unittest.TestCase):
     layer = PLONE_INTEGRATION_TESTING
 
     def setUp(self):
-        self.portal = self.layer['portal']
-        self.request = self.layer['request']
-        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+        self.portal = self.layer["portal"]
+        self.request = self.layer["request"]
+        setRoles(self.portal, TEST_USER_ID, ["Manager"])
 
-    @mock.patch('Products.CMFPlone.controlpanel.browser.overview.getUtility',
-                new=mock_getUtility1)
+    @mock.patch(
+        "Products.CMFPlone.controlpanel.browser.overview.getUtility",
+        new=mock_getUtility1,
+    )
     def test_timezone_warning__noreg(self):
         # If no registry key is available, return True
         registry = getUtility(IRegistry)
         reg_key = "plone.portal_timezone"
         del registry.records[reg_key]
         self.assertFalse(reg_key in registry)
-        view = self.portal.restrictedTraverse('@@overview-controlpanel')
+        view = self.portal.restrictedTraverse("@@overview-controlpanel")
         self.assertTrue(view.timezone_warning())
 
-    @mock.patch('Products.CMFPlone.controlpanel.browser.overview.getUtility',
-                new=mock_getUtility2)
+    @mock.patch(
+        "Products.CMFPlone.controlpanel.browser.overview.getUtility",
+        new=mock_getUtility2,
+    )
     def test_timezone_warning__emptyreg(self):
         # If registry key value is empty, return True
         registry = getUtility(IRegistry)
         reg_key = "plone.portal_timezone"
         registry[reg_key] = None
-        view = self.portal.restrictedTraverse('@@overview-controlpanel')
+        view = self.portal.restrictedTraverse("@@overview-controlpanel")
         self.assertTrue(view.timezone_warning())
 
-    @mock.patch('Products.CMFPlone.controlpanel.browser.overview.getUtility',
-                new=mock_getUtility3)
+    @mock.patch(
+        "Products.CMFPlone.controlpanel.browser.overview.getUtility",
+        new=mock_getUtility3,
+    )
     def test_timezone_warning__set(self):
         # If new plone.portal_timezone is set, return False
-        view = self.portal.restrictedTraverse('@@overview-controlpanel')
+        view = self.portal.restrictedTraverse("@@overview-controlpanel")
         self.assertFalse(view.timezone_warning())
+
+    def test_gunicorn_server_name(self):
+        self.request["SERVER_SOFTWARE"] = "gunicorn/19.6.0"
+        view = self.portal.restrictedTraverse("@@overview-controlpanel")
+        self.assertEqual(view.server_info()["server_name"], "gunicorn")