tool_panel_manager: route shed installs through populate_for_paths

``add_to_tool_panel`` now writes ``shed_tool_conf.xml`` *before* loading
items into the toolbox, then calls
``galaxy.tool_source_store.populator.populate_for_paths`` on the new
tool file paths. The populator writes ``StoredToolSource`` +
``ToolIndexEntry`` + the whoosh index for each path and broadcasts
``reload_tool_source_cache`` so peer Galaxy processes refresh.

After the populator returns, the in-process toolbox's
``invalidate_index_cache`` is called synchronously so this process sees
the new entries immediately (the install API response can't wait for
AMQP loopback). Commit 11's stub-registration runs inside that call.

``load_item`` is still invoked per elem so the in-memory ``_tool_panel``
and ``_integrated_tool_panel`` get the standard panel bookkeeping —
``create_tool``'s index lookup now succeeds (commit 10's raise stays
quiet) because the populator just wrote the entry.

New helper ``_collect_new_tool_paths`` walks ``elem_list``, flattening
``<section>`` children, and returns absolute paths matching what
``discover_tools`` yields after the conf is on disk.
This commit is contained in:
mvdbeek
2026-07-28 17:27:17 +02:00
parent ed1fa36674
commit 57901fcd0e
@@ -1,5 +1,6 @@
import errno
import logging
import os
from typing import (
Any,
)
@@ -22,6 +23,30 @@ from galaxy.util.tool_shed.xml_util import parse_xml
log = logging.getLogger(__name__)
def _collect_new_tool_paths(elem_list, tool_path: str) -> list[str]:
"""Walk ``elem_list`` and return the absolute paths of every ``<tool>``.
``elem_list`` is the freshly-generated panel additions for a shed install
— either top-level ``<tool>`` elements or ``<section>`` elements with
nested ``<tool>`` children. Paths are ``os.path.join(tool_path, file)``,
matching what ``galaxy.tool_source_store.discover.discover_tools`` yields
after the conf is rewritten on disk.
"""
new_paths: list[str] = []
for elem in elem_list:
if elem.tag == "tool":
relative = elem.get("file")
if relative:
new_paths.append(os.path.normpath(os.path.join(tool_path, relative)))
elif elem.tag == "section":
for child in elem:
if child.tag == "tool":
relative = child.get("file")
if relative:
new_paths.append(os.path.normpath(os.path.join(tool_path, relative)))
return new_paths
class ToolPanelManager:
app: InstallationTarget
@@ -115,22 +140,43 @@ class ToolPanelManager:
)
if new_install:
tool_path = shed_tool_conf_dict["tool_path"]
# Add the new elements to the shed_tool_conf file on disk.
# Build the new in-memory list of config_elems. We persist the
# updated shed_tool_conf.xml *before* invoking the populator so
# ``discover_tools`` sees the new entries when it walks the confs.
config_elems = shed_tool_conf_dict["config_elems"]
for config_elem in elem_list:
# Add the new elements to the in-memory list of config_elems.
config_elems.append(config_elem)
# Load the tools into the in-memory tool panel.
shed_tool_conf_dict["config_elems"] = config_elems
self.app.toolbox.update_shed_config(shed_tool_conf_dict)
self.add_to_shed_tool_config(shed_tool_conf_dict, elem_list)
# Populator writes ``StoredToolSource`` + ``ToolIndexEntry`` +
# whoosh for every new tool file, then broadcasts
# ``reload_tool_source_cache`` so peer Galaxy processes refresh.
# ``create_tool`` raises on index miss, so this MUST run before
# ``load_item`` reaches the seam.
new_paths = _collect_new_tool_paths(elem_list, tool_path)
if new_paths:
from galaxy.tool_source_store.populator import populate_for_paths
populate_for_paths(
self.app.config,
self.app.model.context,
paths=new_paths,
rebuild_whoosh=True,
)
# Refresh THIS process synchronously; the AMQP broadcast
# above only reaches peers asynchronously, but the install
# response should reflect the new tools immediately.
self.app.toolbox.invalidate_index_cache()
# Wire the new tools into the in-memory panel. ``create_tool``
# now finds them in the index and hands back ``LazyTool`` stubs.
for config_elem in elem_list:
self.app.toolbox.load_item(
config_elem,
tool_path=tool_path,
load_panel_dict=True,
guid=config_elem.get("guid"),
)
# Replace the old list of in-memory config_elems with the new list for this shed_tool_conf_dict.
shed_tool_conf_dict["config_elems"] = config_elems
self.app.toolbox.update_shed_config(shed_tool_conf_dict)
self.add_to_shed_tool_config(shed_tool_conf_dict, elem_list)
def config_elems_to_xml_file(self, config_elems, config_filename, tool_path) -> None:
"""