This is an automated email from the ASF dual-hosted git repository.
mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 11b6d12386 Issue #8601 : Do not crash when opening a workflow
execution viewer (#8602)
11b6d12386 is described below
commit 11b6d12386ece077ec6542d6893502504425c120
Author: Matt Casters <[email protected]>
AuthorDate: Mon Sep 28 12:26:12 2026 +0200
Issue #8601 : Do not crash when opening a workflow execution viewer (#8602)
* Issue #8601 : Do not crash when opening a workflow execution viewer
* Issue #8601 : Discard a failed execution viewer open
Drop the tab-repair and stale-viewer paths. A viewer tab cannot lose its
data once it is stored before setup, and closing a tab already unregisters the
viewer. A failed open now cleans up on any throwable, and the test lives in rcp
so it can call addViewer.
---
.../ExecutionPerspectiveActiveViewerTest.java | 176 +++++++++++++++++++++
.../execution/ExecutionPerspective.java | 84 ++++++++--
.../execution/WorkflowExecutionViewer.java | 27 ++--
.../hop/ui/hopgui/shared/BaseExecutionViewer.java | 2 +-
4 files changed, 264 insertions(+), 25 deletions(-)
diff --git
a/rcp/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspectiveActiveViewerTest.java
b/rcp/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspectiveActiveViewerTest.java
new file mode 100644
index 0000000000..0b662684d4
--- /dev/null
+++
b/rcp/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspectiveActiveViewerTest.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.perspective.execution;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.reflect.Field;
+import org.apache.hop.ui.testing.SwtBotTestBase;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabItem;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Double-clicking a workflow execution selects its viewer tab. A tab with no
data used to throw
+ * from {@code CTabItem.getData().equals} (issue #8601). Opening a viewer
whose setup fails must not
+ * leave that composite parented to the folder.
+ */
+@Tag("uitest")
+class ExecutionPerspectiveActiveViewerTest extends SwtBotTestBase {
+
+ private Shell shell;
+ private CTabFolder folder;
+ private ExecutionPerspective perspective;
+ private ExecutionPerspective previousInstance;
+
+ @BeforeEach
+ void openFolder() throws Exception {
+ previousInstance = currentInstance();
+ perspective = new ExecutionPerspective();
+ shell = new Shell(display);
+ folder = new CTabFolder(shell, SWT.CLOSE);
+ setField(perspective, "tabFolder", folder);
+ }
+
+ @AfterEach
+ void closeFolder() throws Exception {
+ if (shell != null && !shell.isDisposed()) {
+ shell.dispose();
+ }
+ setInstance(previousInstance);
+ }
+
+ @Test
+ void nullDataTabDoesNotHideTheOpenViewer() {
+ StubViewer workflow = new StubViewer("lees-van-kafka", "dae9c3e9");
+ CTabItem empty = new CTabItem(folder, SWT.CLOSE);
+ CTabItem workflowTab = new CTabItem(folder, SWT.CLOSE);
+ workflowTab.setText(workflow.getName());
+ workflowTab.setData(workflow);
+
+ assertDoesNotThrow(() -> perspective.setActiveViewer(workflow));
+
+ assertSame(workflow, perspective.getActiveViewer());
+ assertSame(workflowTab, folder.getSelection());
+ assertEquals(1, workflow.focusCount);
+ assertNull(empty.getData());
+ }
+
+ @Test
+ void nullViewerDoesNothing() {
+ assertDoesNotThrow(() -> perspective.setActiveViewer(null));
+ assertNull(perspective.getActiveViewer());
+ }
+
+ @Test
+ void failedOpenLeavesNoTabAndDisposesTheViewer() {
+ Composite body = new Composite(folder, SWT.NONE);
+ StubViewer workflow = new StubViewer("lees-van-kafka", "dae9c3e9");
+ workflow.control = body;
+ workflow.imageFailure = new Error("icon");
+
+ Error failure = assertThrows(Error.class, () ->
perspective.addViewer(workflow));
+
+ assertSame(workflow.imageFailure, failure);
+ assertEquals(0, folder.getItemCount());
+ assertNull(perspective.findViewer(workflow.getLogChannelId(),
workflow.getName()));
+ assertTrue(body.isDisposed());
+ }
+
+ private static ExecutionPerspective currentInstance() throws Exception {
+ Field field = ExecutionPerspective.class.getDeclaredField("instance");
+ field.setAccessible(true);
+ return (ExecutionPerspective) field.get(null);
+ }
+
+ private static void setInstance(ExecutionPerspective instance) throws
Exception {
+ Field field = ExecutionPerspective.class.getDeclaredField("instance");
+ field.setAccessible(true);
+ field.set(null, instance);
+ }
+
+ private static void setField(Object target, String name, Object value)
throws Exception {
+ Field field = target.getClass().getDeclaredField(name);
+ field.setAccessible(true);
+ field.set(target, value);
+ }
+
+ /** Viewer stand-in that does not build a workflow canvas. */
+ private static final class StubViewer implements IExecutionViewer {
+ private final String name;
+ private final String id;
+ private Control control;
+ private int focusCount;
+ private Error imageFailure;
+
+ private StubViewer(String name, String id) {
+ this.name = name;
+ this.id = id;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getLogChannelId() {
+ return id;
+ }
+
+ @Override
+ public Image getTitleImage() {
+ if (imageFailure != null) {
+ throw imageFailure;
+ }
+ return null;
+ }
+
+ @Override
+ public String getTitleToolTip() {
+ return null;
+ }
+
+ @Override
+ public boolean setFocus() {
+ focusCount++;
+ return true;
+ }
+
+ @Override
+ public Control getControl() {
+ return control;
+ }
+
+ @Override
+ public void refresh() {}
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
index 2bb85d595c..3434f8c9e3 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
@@ -506,16 +506,31 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
}
public void addViewer(IExecutionViewer viewer) {
- // Create tab item
+ if (viewer == null || tabFolder == null || tabFolder.isDisposed()) {
+ return;
+ }
+
+ // Data is set before any call that can throw. A tab left without data
makes the next
+ // double-click crash in setActiveViewer (issue #8601).
//
CTabItem tabItem = new CTabItem(tabFolder, SWT.CLOSE);
- tabItem.setFont(GuiResource.getInstance().getFontDefault());
- tabItem.setText(viewer.getName());
- tabItem.setImage(viewer.getTitleImage());
- tabItem.setToolTipText(viewer.getTitleToolTip());
-
- tabItem.setControl(viewer.getControl());
- tabItem.setData(viewer);
+ boolean ok = false;
+ try {
+ tabItem.setData(viewer);
+ tabItem.setFont(GuiResource.getInstance().getFontDefault());
+ tabItem.setText(Const.NVL(viewer.getName(), ""));
+ tabItem.setImage(viewer.getTitleImage());
+ tabItem.setToolTipText(viewer.getTitleToolTip());
+ Control control = viewer.getControl();
+ if (control != null && !control.isDisposed()) {
+ tabItem.setControl(control);
+ }
+ ok = true;
+ } finally {
+ if (!ok) {
+ discardViewerTab(tabItem, viewer);
+ }
+ }
viewers.add(viewer);
@@ -534,6 +549,32 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
viewer.refresh();
}
+ /**
+ * Drop a tab that never became a usable viewer. The viewer composite is a
child of the folder
+ * even when it was not attached to the tab, so it is disposed too.
+ */
+ private void discardViewerTab(CTabItem tabItem, IExecutionViewer viewer) {
+ if (tabItem != null && !tabItem.isDisposed()) {
+ try {
+ tabItem.setControl(null);
+ } catch (RuntimeException e) {
+ // Detach is best-effort; the tab is about to be disposed.
+ }
+ tabItem.dispose();
+ }
+ if (viewer == null) {
+ return;
+ }
+ try {
+ Control control = viewer.getControl();
+ if (control != null && !control.isDisposed()) {
+ control.dispose();
+ }
+ } catch (RuntimeException e) {
+ // The viewer is already unusable; the open fails with the original
exception.
+ }
+ }
+
/**
* Find a metadata editor
*
@@ -553,22 +594,33 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
}
public void setActiveViewer(IExecutionViewer viewer) {
+ if (viewer == null || tabFolder == null || tabFolder.isDisposed()) {
+ return;
+ }
for (CTabItem item : tabFolder.getItems()) {
- if (item.getData().equals(viewer)) {
+ if (item == null || item.isDisposed()) {
+ continue;
+ }
+ // Compare from the viewer. A tab with no data must not throw (issue
#8601).
+ //
+ if (viewer.equals(item.getData())) {
tabFolder.setSelection(item);
tabFolder.showItem(item);
-
viewer.setFocus();
}
}
}
public IExecutionViewer getActiveViewer() {
- if (tabFolder.getSelectionIndex() < 0) {
+ if (tabFolder == null || tabFolder.isDisposed() ||
tabFolder.getSelectionIndex() < 0) {
return null;
}
- return (IExecutionViewer) tabFolder.getSelection().getData();
+ Object data = tabFolder.getSelection().getData();
+ if (data instanceof IExecutionViewer viewer) {
+ return viewer;
+ }
+ return null;
}
protected void onTabClose(CTabFolderEvent event) {
@@ -1554,9 +1606,11 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
@Override
public void closeTab(CTabFolderEvent event, CTabItem tabItem) {
- IExecutionViewer viewer = (IExecutionViewer) tabItem.getData();
-
- boolean isRemoved = viewers.remove(viewer);
+ if (tabItem == null || tabItem.isDisposed()) {
+ return;
+ }
+ Object data = tabItem.getData();
+ boolean isRemoved = data instanceof IExecutionViewer viewer &&
viewers.remove(viewer);
tabItem.dispose();
if (isRemoved) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
index 3ab912c293..124a3618f2 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
@@ -333,17 +333,26 @@ public class WorkflowExecutionViewer extends
BaseExecutionViewer
if (childIds != null) {
for (String id : childIds) {
ExecutionData actionData =
iLocation.getExecutionData(execution.getId(), id);
+ // A child id without sample data is normal while an action is still
starting. Skipping
+ // it keeps the workflow info tab on screen instead of failing the
whole refresh.
+ //
+ if (actionData == null) {
+ LogChannel.UI.logDebug("No execution data yet for action id '" +
id + "'");
+ continue;
+ }
ExecutionDataSetMeta dataSetMeta = actionData.getDataSetMeta();
- if (dataSetMeta != null) {
- String actionName = dataSetMeta.getName();
-
- // Add this one under that name
- //
- List<ExecutionData> executionDataList =
- actionExecutions.computeIfAbsent(actionName, k -> new
ArrayList<>());
- executionDataList.add(actionData);
+ if (dataSetMeta == null || dataSetMeta.getName() == null) {
+ LogChannel.UI.logDebug("Execution data for action id '" + id + "'
has no action name");
+ continue;
}
+ String actionName = dataSetMeta.getName();
+
+ // Add this one under that name
+ //
+ List<ExecutionData> executionDataList =
+ actionExecutions.computeIfAbsent(actionName, k -> new
ArrayList<>());
+ executionDataList.add(actionData);
}
}
} catch (Exception e) {
@@ -812,7 +821,7 @@ public class WorkflowExecutionViewer extends
BaseExecutionViewer
@Override
public String getActiveId() {
- if (selectedAction != null) {
+ if (selectedAction != null && selectedExecutionData != null) {
if (selectedExecutionData.getOwnerId() == null) {
return selectedExecutionData.getParentId();
} else {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
index 7d6816afae..3c5efefe69 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
@@ -113,7 +113,7 @@ public abstract class BaseExecutionViewer extends
DragViewZoomBase
@Override
public boolean setFocus() {
- if (canvas.isDisposed()) {
+ if (canvas == null || canvas.isDisposed()) {
return false;
}
return canvas.setFocus();