Skip to content

Commit 12fd4aa

Browse files
authored
Merge pull request #207 from Keck-DataReductionPipelines/develop
Develop
2 parents 8a07454 + 6058c37 commit 12fd4aa

File tree

6 files changed

+423
-27
lines changed

6 files changed

+423
-27
lines changed

kcwidrp/configs/kcwi.cfg

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ enable_bokeh = True
2323
plot_level = 1
2424
# How long to pause between plots at plot_level = 1 (seconds)
2525
plot_pause = 1
26+
# Use the Firefox-compatible plot export path. The simple export (False) works
27+
# for most users; set to True only on systems where Bokeh's default browser
28+
# detection fails (e.g. Ubuntu 24.04 with snap-confined Firefox).
29+
# Stop the pipeline if the Bokeh server fails to start. When False, the
30+
# pipeline continues without interactive plots and logs a warning instead.
31+
terminate_on_failed_bokeh_start = False
32+
plot_firefox_compat = False
33+
# Pre-warm the Firefox WebDriver at pipeline startup so the first save_plot
34+
# call does not pay the browser startup cost. Only relevant when
35+
# plot_firefox_compat = True.
36+
plot_prewarm_firefox = False
2637
# Wavelength fitting verbosity: 1 - normal, 2 - verbose
2738
verbose = 1
2839
# Bokeh plot dimensions in pixels

kcwidrp/core/bokeh_plotting.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ def bokeh_plot(plot, session):
2020

2121
# NOT TESTED YET
2222

23+
if session is None:
24+
return
25+
2326
new_figure = session.document.select_one(selector=dict(type=Figure))
2427
layout = session.document.select_one(selector=dict(type=Column))
2528

kcwidrp/core/kcwi_plotting.py

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,72 @@
11
import numpy as np
22
from bokeh.io import export_png
33

4+
import atexit
45
import os
6+
import shutil
57
import logging
68

9+
from selenium import webdriver
10+
711
logger = logging.getLogger('KCWI')
812

13+
_firefox_driver = None
14+
15+
16+
def configure_plot_driver(firefox_compat=False, prewarm=False):
17+
"""Optionally pre-warm the Firefox WebDriver at pipeline startup.
18+
19+
Called by StartBokeh when both plot_firefox_compat and plot_prewarm_firefox
20+
are True in kcwi.cfg. Once the driver is created here, save_plot will
21+
automatically use the Firefox-compatible export path for all subsequent
22+
calls.
23+
"""
24+
if firefox_compat and prewarm:
25+
logger.info("Pre-warming Firefox WebDriver for plot export")
26+
_get_driver()
27+
28+
29+
def _get_driver():
30+
"""Return a cached Firefox WebDriver, creating it on first call."""
31+
global _firefox_driver
32+
if _firefox_driver is not None:
33+
try:
34+
# Navigate to about:blank to cancel any pending navigation left by
35+
# Bokeh's reset step (it navigates to http://0.0.0.1/ after each
36+
# screenshot; on some networks that address times out rather than
37+
# refusing immediately, blocking the next export call).
38+
_firefox_driver.get("about:blank")
39+
return _firefox_driver
40+
except Exception:
41+
_firefox_driver = None
42+
43+
from selenium.webdriver.firefox.service import Service as FirefoxService
44+
options = webdriver.FirefoxOptions()
45+
options.add_argument("--headless")
46+
options.add_argument("--no-sandbox")
47+
options.add_argument("--disable-dev-shm-usage")
48+
options.add_argument("--hide-scrollbars")
49+
options.add_argument("--force-device-scale-factor=1")
50+
options.add_argument("--force-color-profile=srgb")
51+
firefox_bin = _find_firefox_binary()
52+
if firefox_bin:
53+
options.binary_location = firefox_bin
54+
geckodriver = _find_geckodriver()
55+
serv = FirefoxService(executable_path=geckodriver) if geckodriver else FirefoxService()
56+
_firefox_driver = webdriver.Firefox(options=options, service=serv)
57+
atexit.register(_close_driver)
58+
return _firefox_driver
59+
60+
61+
def _close_driver():
62+
global _firefox_driver
63+
if _firefox_driver is not None:
64+
try:
65+
_firefox_driver.quit()
66+
except Exception:
67+
pass
68+
_firefox_driver = None
69+
970

1071
def get_plot_lims(data, padding=0.05, clip=True):
1172
"""Get plot limits using data range plus padding fraction"""
@@ -33,12 +94,78 @@ def set_plot_lims(fig, xlim=None, ylim=None):
3394
fig.y_range.start = ylim[0]
3495
fig.y_range.end = ylim[1]
3596

97+
def _find_firefox_binary():
98+
"""Return path to the real Firefox binary, handling snap wrapper installs."""
99+
# On Ubuntu 24.04, /usr/bin/firefox is a shell script wrapper for the snap.
100+
# Selenium requires the actual ELF binary, not a wrapper script.
101+
candidates = [
102+
'/snap/firefox/current/usr/lib/firefox/firefox',
103+
'/usr/lib/firefox/firefox',
104+
]
105+
for path in candidates:
106+
if os.path.isfile(path) and os.access(path, os.X_OK):
107+
return path
108+
# Fall back to whatever is on PATH (works on most non-snap installs)
109+
return shutil.which('firefox')
110+
111+
112+
def _find_geckodriver():
113+
"""Return path to geckodriver, searching common locations."""
114+
candidates = [
115+
'/snap/bin/geckodriver',
116+
'/usr/bin/geckodriver',
117+
'/usr/local/bin/geckodriver',
118+
]
119+
for path in candidates:
120+
if os.path.isfile(path) and os.access(path, os.X_OK):
121+
return path
122+
return shutil.which('geckodriver')
123+
36124

37125
def save_plot(fig, filename=None):
126+
"""Save a Bokeh figure to a PNG file.
127+
128+
Dispatches to the Firefox-compatible path or the simple path depending on
129+
whether configure_plot_driver() was called with firefox_compat=True.
130+
"""
131+
if _firefox_driver is not None:
132+
_save_plot_firefox(fig, filename)
133+
else:
134+
_save_plot_simple(fig, filename)
135+
136+
137+
def _save_plot_firefox(fig, filename=None):
138+
"""Firefox-compatible export for systems with snap-confined Firefox."""
38139
if filename is None:
39140
fnam = os.path.join('plots', 'kcwi_drp_plot.png')
40141
else:
41142
fnam = os.path.join('plots', filename)
42-
export_png(fig, filename=fnam)
43143

144+
# Resolve to absolute path before chdir so the PNG lands in the right place.
145+
fnam = os.path.abspath(fnam)
146+
os.makedirs(os.path.dirname(fnam), exist_ok=True)
147+
148+
driver = _get_driver()
149+
# Snap-confined Firefox on Ubuntu 24.04 uses a private /tmp mount namespace,
150+
# so file:///tmp/... URLs are invisible to it. $HOME is accessible via the
151+
# snap 'home' interface. Bokeh writes its scratch HTML relative to the
152+
# process CWD, so chdir to $HOME to land the temp file somewhere Firefox
153+
# can actually reach. fnam is already absolute so the PNG output goes to
154+
# the right place regardless.
155+
old_cwd = os.getcwd()
156+
try:
157+
os.chdir(os.path.expanduser('~'))
158+
export_png(fig, filename=fnam, webdriver=driver)
159+
finally:
160+
os.chdir(old_cwd)
161+
logger.info(">>> Saving to %s" % fnam)
162+
163+
164+
def _save_plot_simple(fig, filename=None):
165+
"""Simple export using Bokeh's built-in browser detection."""
166+
if filename is None:
167+
fnam = os.path.join('plots', 'kcwi_drp_plot.png')
168+
else:
169+
fnam = os.path.join('plots', filename)
170+
export_png(fig, filename=fnam)
44171
logger.info(">>> Saving to %s" % fnam)

kcwidrp/primitives/StartBokeh.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from bokeh.plotting.figure import figure
55
from bokeh.layouts import column
66

7+
from kcwidrp.core.kcwi_plotting import configure_plot_driver
8+
79

810
class StartBokeh(BasePrimitive):
911
"""
@@ -23,8 +25,18 @@ def __init__(self, action, context):
2325

2426
def _perform(self):
2527

26-
# session = pull_session(session_id='kcwi', url='http://localhost:5006')
27-
session = pull_session()
28+
self.context.bokeh_session = None
29+
try:
30+
# session = pull_session(session_id='kcwi', url='http://localhost:5006')
31+
session = pull_session()
32+
except Exception as e:
33+
if getattr(self.config.instrument, 'terminate_on_failed_bokeh_start', False):
34+
self.logger.error("Could not connect to Bokeh server: %s", e)
35+
raise RuntimeError("Bokeh server failed to start and "
36+
"terminate_on_failed_bokeh_start is True") from e
37+
self.logger.warning("Could not connect to Bokeh server, "
38+
"interactive plots disabled: %s", e)
39+
return self.action.args
2840
self.logger.info("Enabling BOKEH plots")
2941
p = figure()
3042
c = column(children=[p])
@@ -33,4 +45,9 @@ def _perform(self):
3345
self.context.bokeh_session = session
3446
session.show(c)
3547

48+
firefox_compat = getattr(self.config.instrument, 'plot_firefox_compat', False)
49+
prewarm = getattr(self.config.instrument, 'plot_prewarm_firefox', False)
50+
if firefox_compat and prewarm:
51+
configure_plot_driver(firefox_compat=firefox_compat, prewarm=prewarm)
52+
3653
return True

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "kcwidrp"
3-
version = "1.3.0"
3+
version = "1.3.1"
44
description = "KCWI Data Reduction Pipeline"
55
readme = "README.rst"
66
license = "BSD-3-Clause"

0 commit comments

Comments
 (0)