Repository: bokeh/bokeh

Issue:
webdriver failing to find installed firefox/geckodriver
Can't get export examples running on binder even though everything is installed and on the path:

<img width="1130" alt="Screen Shot 2020-06-02 at 8 23 16 PM" src="https://user-images.githubusercontent.com/1078448/83592297-e9459c80-a50e-11ea-90d1-7189fcc93af0.png">




Discussion:
Noting the failure is in `webdriver.Firefox` with:
```
SessionNotCreatedException: Message: Unable to find a matching set of capabilities
```
Users reporting a similar problem on Collab
Hello! 
Reporting the same issue in my case as well. 
The export png method fails in colab setup. I have installed selenium/chromedriver etc. But the export method fails every time with this error. 

> Neither firefox and geckodriver nor a variant of chromium browser and chromedriver are available on system PATH. You can install the former with 'conda install -c conda-forge firefox geckodriver'.

I am simply using this command
```
x = export_png(plt, filename=Path + '/Print my Plot.png')
```


@vivek1383 can you poke around and try to locate where the web driver and browser actually get installed on collab? It's not something we have control over (and not something I have any immediate offhand knowledge of). 
Sure let me try. I did try to look for it in /content/node_modules/ But I couldnt. Let me poke around more. 

Is there any way (during pip install) I can find out when its installed?
Okay followed this post https://stackoverflow.com/questions/60686871/colab-cant-find-chromedriver-path and now I find the chrome driver here. "/usr/bin/chromedriver". firefox bin is also in the same directory. 
Hello team, 
Any help will be appreciated here. I think if I can get any pointers to what the system path variable is, I may be able to proceed. 

Thanks in advance. 
@vivek1383 It's `PATH` on all platforms, AFAIK
ok, thanks. Let me try that. 
The problem is that `bin/firefox` is a script (in conda's case trivial `bin/FirefoxApp/firefox "@"`), and webdriver expects an executable binary file. I'm not sure why it's failing now. Perhaps the script is a new addition or geckodriver is stricter now. In any case, the solution is to provide a path to the real binary to geckodriver, which will be fun to maintain across platforms, etc.

Resolving patch:
diff --git a/bokeh/io/webdriver.py b/bokeh/io/webdriver.py
--- a/bokeh/io/webdriver.py
+++ b/bokeh/io/webdriver.py
@@ -25,8 +25,9 @@
 
 # Standard library imports
 import atexit
-import shutil
-from os.path import devnull
+import os
+from os.path import devnull, dirname, isfile, join
+from shutil import which
 from typing import List, Optional
 
 # External imports
@@ -53,11 +54,38 @@
 #-----------------------------------------------------------------------------
 
 def create_firefox_webdriver() -> WebDriver:
+    firefox = which("firefox")
+    if firefox is None:
+        raise RuntimeError("firefox is not installed or not present on PATH")
+
+    geckodriver = which("geckodriver")
+    if geckodriver is None:
+        raise RuntimeError("geckodriver is not installed or not present on PATH")
+
+    firefox_paths = [
+        join(dirname(firefox), "FirefoxApp", "firefox"),
+        join(dirname(firefox), "FirefoxApp", "Contents", "MacOS", "firefox"),
+    ]
+
+    for firefox_path in firefox_paths:
+        if _is_executable(firefox_path):
+            binary_path = firefox_path
+            break
+    else:
+        binary_path = firefox
+
     from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
-    binary = FirefoxBinary(_detect("firefox"))
+    binary = FirefoxBinary(binary_path)
+
     options = webdriver.firefox.options.Options()
     options.add_argument("--headless")
-    return webdriver.Firefox(firefox_binary=binary, options=options, service_log_path=devnull)
+
+    return webdriver.Firefox(
+        options=options,
+        firefox_binary=binary,
+        executable_path=geckodriver,
+        service_log_path=devnull,
+    )
 
 def create_chromium_webdriver() -> WebDriver:
     options = webdriver.chrome.options.Options()
@@ -71,8 +99,8 @@ def create_chromium_webdriver() -> WebDriver:
 # Private API
 #-----------------------------------------------------------------------------
 
-def _detect(executable: str) -> Optional[str]:
-    return shutil.which(executable)
+def _is_executable(path: str) -> bool:
+    return isfile(path) and os.access(path, os.X_OK)
 
 def _try_create_firefox_webdriver() -> Optional[WebDriver]:
     try:


Test patch: