Repository: voxel51/fiftyone
Issue: [FR] Validate desktop package version when launching desktop App

Suppose I am a civilian desktop App user of `fiftyone==0.11.2.1`, which I installed as follows:

```shell
pip install fiftyone==0.11.2.1
pip install 'fiftyone[desktop]'
```

Then I upgrade my FiftyOne package [as instructed](https://voxel51.com/docs/fiftyone/getting_started/install.html#upgrading-fiftyone):

```shell
pip install --upgrade fiftyone
```

If I then try to launch the App:

```shell
fiftyone quickstart --desktop
```

I see an unhandled error like so:

```
An exception has been raised by the server. Your session has been reverted to its previous state.

Traceback (most recent call last):
  File "main.py", line 272, in wrapper
    result = await func(self, *args, **kwargs)
  File "main.py", line 516, in on_message
    event = getattr(self, "on_%s" % message.pop("type"))
AttributeError: 'StateHandler' object has no attribute 'on_page'
```

The solution is to upgrade my `fiftyone-desktop` package too, but, as a civilian, I didn't know that I needed to do that and the error message above doesn't help.

Proposals:
- Should we manually verify that the desktop package has a compliant version before launching the App, and, if not, print a helpful error message telling the user what to do? And/or, is there something more automatic we can do to always keep the desktop package in sync?
- And/or, add `fiftyone-desktop` upgrade instructions to [upgrade section](https://voxel51.com/docs/fiftyone/getting_started/install.html#upgrading-fiftyone)?

Related note: 

The [install guide](https://voxel51.com/docs/fiftyone/getting_started/install.html#fiftyone-desktop-app) shows `pip install fiftyone-desktop`, but `pip install 'fiftyone[desktop]'` would probably be better because the latter will install the proper desktop version for your `fiftyone` install, while the former seems to always install the latest version of the desktop package.


Changed files:
fiftyone/core/session.py

diff --git a/fiftyone/core/session.py b/fiftyone/core/session.py
index fc09eadcc..cb5374d3e 100644
--- a/fiftyone/core/session.py
+++ b/fiftyone/core/session.py
@@ -8,6 +8,7 @@ Session class for interacting with the FiftyOne App.
 from collections import defaultdict
 from functools import wraps
 import logging
+import pkg_resources
 import time
 from uuid import uuid4
 import webbrowser
@@ -340,14 +341,7 @@ class Session(foc.HasClient):
                     "Cannot open a Desktop App instance from a Colab notebook"
                 )
 
-            try:
-                import fiftyone.desktop  # pylint: disable=unused-import
-            except ImportError as e:
-                if not focn.DEV_INSTALL:
-                    raise ValueError(
-                        "You must install the 'fiftyone-desktop' package "
-                        "in order to launch a desktop App instance"
-                    ) from e
+            _import_desktop()
 
             self._app_service = fos.AppService(server_port=port)
             return
@@ -356,6 +350,9 @@ class Session(foc.HasClient):
             self.open()
             return
 
+    
+        
+        
     def _validate(self, dataset, view, plots, config):
         if dataset is not None and not isinstance(dataset, fod.Dataset):
             raise ValueError(
@@ -1074,3 +1071,32 @@ def _display_colab(session, handle, uuid, port, height, update=False):
             )
 
     output.register_callback("fiftyone.%s" % uuid.replace("-", "_"), capture)
+
+
+def _import_desktop():
+        try:
+            import fiftyone.desktop
+        except ImportError as e:
+            raise ValueError(
+                "You must `pip install fiftyone[desktop]` in order to launch the "
+                "desktop App"
+            ) from e
+
+        # Get `fiftyone-desktop` requirement for current `fiftyone` install
+        fiftyone_dist = pkg_resources.get_distribution("fiftyone")
+        requirements = fiftyone_dist.requires(extras=["desktop"])
+        desktop_req = [r for r in requirements if r.name == "fiftyone-desktop"][0]
+
+        desktop_dist = pkg_resources.get_distribution("fiftyone-desktop")
+
+        if not desktop_req.specifier.contains(desktop_dist.version):
+            raise ValueError(
+                "fiftyone==%s requires fiftyone-desktop%s, but you have "
+                "fiftyone-desktop==%s installed.\\n"
+                "Run `pip install fiftyone[desktop]` to install the proper "
+                "desktop package version" % (
+                    fiftyone_dist.version,
+                    desktop_req.specifier,
+                    desktop_dist.version
+                )
+            )