Rework batch actions

This commit is contained in:
crschnick
2026-02-02 16:05:43 +00:00
parent b4c2422a5b
commit 605ff32b3a
17 changed files with 185 additions and 52 deletions
@@ -20,6 +20,10 @@ public interface BatchHubProvider<T extends DataStore> extends ActionProvider {
Class<?> getApplicableClass();
default boolean requiresValidStore() {
return true;
}
default boolean isApplicable(DataStoreEntryRef<T> o) {
return true;
}
@@ -39,7 +43,11 @@ public interface BatchHubProvider<T extends DataStore> extends ActionProvider {
})
.filter(action -> action != null)
.toList();
return BatchStoreAction.<T>builder().actions(individual).build();
return BatchStoreAction.<T>builder().actions(individual).parallel(runParallel()).build();
}
default boolean runParallel() {
return false;
}
@SneakyThrows
@@ -2,9 +2,11 @@ package io.xpipe.app.hub.action;
import io.xpipe.app.action.*;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.process.CountDown;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.JacksonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -14,6 +16,7 @@ import lombok.experimental.SuperBuilder;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
@SuperBuilder
@@ -21,6 +24,7 @@ import java.util.stream.Collectors;
public final class BatchStoreAction<T extends DataStore> extends SerializableAction implements StoreContextAction {
private final List<StoreAction<T>> actions;
private boolean parallel;
@Override
public ActionProvider getProvider() {
@@ -40,10 +44,26 @@ public final class BatchStoreAction<T extends DataStore> extends SerializableAct
@Override
public void executeImpl() {
for (AbstractAction action : actions) {
if (!action.executeSyncImpl(true)) {
break;
if (!parallel) {
for (AbstractAction action : actions) {
// Don't confirm twice
if (!action.executeSyncImpl(false)) {
break;
}
}
} else {
var latch = new CountDownLatch(actions.size());
for (AbstractAction action : actions) {
ThreadHelper.runAsync(() -> {
// Don't confirm twice
action.executeSyncImpl(false);
latch.countDown();
});
}
try {
latch.await();
} catch (InterruptedException ignored) {}
}
}
@@ -17,6 +17,11 @@ import lombok.extern.jackson.Jacksonized;
public class OpenHubMenuLeafProvider implements HubLeafProvider<DataStore>, BatchHubProvider<DataStore> {
@Override
public boolean requiresValidStore() {
return true;
}
@Override
public StoreActionCategory getCategory() {
return StoreActionCategory.OPEN;
@@ -13,14 +13,17 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.terminal.*;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.value.ObservableValue;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
public class OpenSplitHubBatchProvider implements BatchHubProvider<ShellStore> {
@@ -65,21 +68,29 @@ public class OpenSplitHubBatchProvider implements BatchHubProvider<ShellStore> {
throw ErrorEventFactory.expected(new IllegalStateException(AppI18n.get("noTerminalSet")));
}
var panes = new ArrayList<TerminalLauncher.Config>();
for (DataStoreEntryRef<ShellStore> ref : getRefs()) {
var replacement = ProcessControlProvider.get().replace(ref);
ShellStore store = replacement.getStore().asNeeded();
var control = store.standaloneControl();
// These prepend scripts, not append
TerminalPromptManager.configurePromptScript(control);
ProcessControlProvider.get().withDefaultScripts(control);
var latch = new CountDownLatch(getRefs().size());
var panes = new TerminalLauncher.Config[getRefs().size()];
for (int i = 0; i < getRefs().size(); i++) {
var ii = i;
var ref = refs.get(i);
ThreadHelper.runFailableAsync(() -> {
try {
var replacement = ProcessControlProvider.get().replace(ref);
ShellStore store = replacement.getStore().asNeeded();
var control = store.standaloneControl();
// These prepend scripts, not append
TerminalPromptManager.configurePromptScript(control);
ProcessControlProvider.get().withDefaultScripts(control);
var title = DataStorage.get().getStoreEntryDisplayName(ref.get());
var config =
new TerminalLauncher.Config(ref.get(), title, null, UUID.randomUUID(), true, true, control);
panes.add(config);
var title = DataStorage.get().getStoreEntryDisplayName(ref.get());
var config = new TerminalLauncher.Config(ref.get(), title, null, UUID.randomUUID(), true, true, control);
panes[ii] = config;
} finally {
latch.countDown();
}
});
}
TerminalLauncher.open(panes, true, type);
TerminalLauncher.open(Arrays.asList(panes), true, type);
}
}
}
@@ -113,6 +113,10 @@ public class StoreEntryListBatchBarComp extends SimpleRegionBuilder {
return false;
}
if (!w.getEntry().getValidity().isUsable() && s.requiresValidStore()) {
return false;
}
if (!s.isApplicable(w.getEntry().ref())) {
return false;
}
@@ -70,10 +70,6 @@ public class StoreViewState {
@Getter
private final DerivedObservableList<StoreEntryWrapper> effectiveBatchModeSelection = batchModeSelection.filtered(
storeEntryWrapper -> {
if (!storeEntryWrapper.getValidity().getValue().isUsable()) {
return false;
}
if (storeEntryWrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) {
return false;
}
@@ -187,8 +183,7 @@ public class StoreViewState {
batchModeSelection.getList().add(wrapper);
}
if (wrapper == null
|| (wrapper.getValidity().getValue().isUsable()
&& wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP)) {
|| wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) {
section.getShownChildren().getList().forEach(c -> selectBatchMode(c));
}
}
@@ -199,8 +194,7 @@ public class StoreViewState {
batchModeSelection.getList().remove(wrapper);
}
if (wrapper == null
|| (wrapper.getValidity().getValue().isUsable()
&& wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP)) {
|| wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) {
section.getShownChildren().getList().forEach(c -> unselectBatchMode(c));
}
}
@@ -8,35 +8,55 @@ import io.xpipe.app.process.ProcessOutputException;
import javafx.scene.control.TextArea;
import javafx.scene.layout.StackPane;
import lombok.Value;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.SequencedMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class CommandDialog {
public static void runMultipleAndShow(Map<String, CommandControl> cmds) {
StringBuilder acc = new StringBuilder();
for (var e : cmds.entrySet()) {
String out;
try {
out = e.getValue().readStdoutOrThrow();
out = formatOutput(out);
} catch (ProcessOutputException ex) {
out = ex.getMessage();
} catch (Throwable t) {
out = ExceptionUtils.getStackTrace(t);
}
@Value
public static class CommandEntry {
acc.append(e.getKey())
.append(" (exit code ")
.append(e.getValue().getExitCode())
.append("):\n")
.append(out)
.append("\n\n");
String name;
CommandControl command;
}
public static void runMultipleAndShow(List<CommandEntry> cmds) {
var parts = new String[cmds.size()];
var latch = new CountDownLatch(parts.length);
for (int i = 0; i < cmds.size(); i++) {
var e = cmds.get(i);
var ii = i;
ThreadHelper.runAsync(() -> {
String out;
try {
out = e.getCommand().readStdoutOrThrow();
out = formatOutput(out);
} catch (ProcessOutputException ex) {
out = ex.getMessage();
} catch (Throwable t) {
out = ExceptionUtils.getStackTrace(t);
}
var s = e.getName() + " (exit code " + e.getCommand().getExitCode() + "):\n" + out;
parts[ii] = s;
latch.countDown();
});
}
show(acc.toString());
try {
latch.await();
} catch (InterruptedException ignored) {}
var joined = String.join("\n\n", parts);
show(joined);
}
public static void runAndShow(CommandControl cmd) {
@@ -137,10 +137,10 @@ public class ScanDialogBase {
stackPane.getStyleClass().add("scan-list");
VBox.setVgrow(stackPane, ALWAYS);
var emptyLabel = new LabelComp(AppI18n.observable("noScanPossible"))
.visible(busy.not().and(Bindings.isEmpty(available)))
.build();
stackPane.getChildren().add(emptyLabel);
if (!showButton) {
var emptyLabel = new LabelComp(AppI18n.observable("noScanPossible")).visible(busy.not().and(Bindings.isEmpty(available))).build();
stackPane.getChildren().add(emptyLabel);
}
Function<ScanProvider.ScanOpportunity, String> nameFunc = (ScanProvider.ScanOpportunity s) -> {
var n = s.getName().getValue();
+2
View File
@@ -72,3 +72,5 @@ The scripting system has been completely reworked with the goal of becoming simp
- Fix ordering for some child connections being random after a restart
- Fix hcloud profile names containing whitespace when multiple profiles were configured
- Fix WinSCP open action requiring an existing ppk key and only working with external key files, not in-place keys
- Fix batch mode selection not working for incomplete connections, like newly added VMs without credentials
- Fix batch action confirmation setting requiring a double confirmation for each individual connection
@@ -10,6 +10,7 @@ import io.xpipe.app.util.CommandDialog;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.ArrayList;
import java.util.LinkedHashMap;
public class RunHubBatchScriptActionProvider implements ActionProvider {
@@ -27,14 +28,14 @@ public class RunHubBatchScriptActionProvider implements ActionProvider {
@Override
public void executeImpl() throws Exception {
var map = new LinkedHashMap<String, CommandControl>();
var list = new ArrayList<CommandDialog.CommandEntry>();
for (DataStoreEntryRef<ShellStore> ref : refs) {
var sc = ref.getStore().getOrStartSession();
var script = scriptStore.getStore().assembleScriptChain(sc, false);
var cmd = sc.command(script);
map.put(ref.get().getName(), cmd);
list.add(new CommandDialog.CommandEntry(ref.get().getName(), cmd));
}
CommandDialog.runMultipleAndShow(map);
CommandDialog.runMultipleAndShow(list);
}
@Override
@@ -168,6 +168,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider<ShellStore
ScriptHierarchy hierarchy;
@Override
public boolean runParallel() {
return true;
}
@Override
public AbstractAction createAction(DataStoreEntryRef<ShellStore> ref) {
return RunTerminalScriptActionProvider.Action.builder()
@@ -176,6 +181,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider<ShellStore
.build();
}
@Override
public boolean requiresValidStore() {
return true;
}
@Override
public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {
return true;
@@ -224,6 +234,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider<ShellStore
ScriptHierarchy hierarchy;
@Override
public boolean requiresValidStore() {
return true;
}
@Override
public AbstractAction createAction(DataStoreEntryRef<ShellStore> ref) {
return RunHubScriptActionProvider.Action.builder()
@@ -285,6 +300,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider<ShellStore
ScriptHierarchy hierarchy;
@Override
public boolean requiresValidStore() {
return true;
}
@Override
public AbstractAction createAction(DataStoreEntryRef<ShellStore> ref) {
return RunBackgroundScriptActionProvider.Action.builder()
@@ -331,6 +351,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider<ShellStore
.build();
}
@Override
public boolean runParallel() {
return true;
}
@Override
public List<ActionProvider> getChildren(List<DataStoreEntryRef<ShellStore>> batch) {
return List.of();
@@ -402,6 +427,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider<ShellStore
private static class NoScriptsActionProvider implements HubLeafProvider<ShellStore>, BatchHubProvider<ShellStore> {
@Override
public boolean requiresValidStore() {
return true;
}
@Override
public void execute(DataStoreEntryRef<ShellStore> ref) {
var cat = StoreViewState.get().getAllScriptsCategory();
@@ -448,6 +478,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider<ShellStore
private static class ScriptsDisabledActionProvider
implements HubLeafProvider<ShellStore>, BatchHubProvider<ShellStore> {
@Override
public boolean requiresValidStore() {
return true;
}
@Override
public void execute(DataStoreEntryRef<ShellStore> ref) {
var cat = StoreViewState.get().getCategoryWrapper(DataStorage.get().getStoreCategory(ref.get()));
@@ -495,6 +530,11 @@ public class RunScriptActionProviderMenu implements HubBranchProvider<ShellStore
private static class NoStateActionProvider implements HubLeafProvider<ShellStore>, BatchHubProvider<ShellStore> {
@Override
public boolean requiresValidStore() {
return true;
}
@Override
public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {
return true;
@@ -17,6 +17,11 @@ import lombok.extern.jackson.Jacksonized;
public class ServiceRefreshHubProvider
implements HubLeafProvider<FixedServiceCreatorStore>, BatchHubProvider<FixedServiceCreatorStore> {
@Override
public boolean requiresValidStore() {
return true;
}
@Override
public StoreActionCategory getCategory() {
return StoreActionCategory.CUSTOM;
@@ -15,6 +15,11 @@ import lombok.extern.jackson.Jacksonized;
public class StorePauseActionProvider implements HubLeafProvider<PauseableStore>, BatchHubProvider<PauseableStore> {
@Override
public boolean runParallel() {
return true;
}
@Override
public StoreActionCategory getCategory() {
return StoreActionCategory.CUSTOM;
@@ -16,6 +16,11 @@ import lombok.extern.jackson.Jacksonized;
public class StoreRestartActionProvider implements HubLeafProvider<DataStore>, BatchHubProvider<DataStore> {
@Override
public boolean runParallel() {
return true;
}
@Override
public StoreActionCategory getCategory() {
return StoreActionCategory.CUSTOM;
@@ -1,5 +1,6 @@
package io.xpipe.ext.base.store;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.hub.action.BatchHubProvider;
import io.xpipe.app.hub.action.HubLeafProvider;
@@ -13,6 +14,8 @@ import javafx.beans.value.ObservableValue;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class StoreStartActionProvider implements HubLeafProvider<StartableStore>, BatchHubProvider<StartableStore> {
@Override
@@ -65,6 +68,11 @@ public class StoreStartActionProvider implements HubLeafProvider<StartableStore>
return "startStore";
}
@Override
public boolean runParallel() {
return true;
}
@Jacksonized
@SuperBuilder
public static class Action extends StoreAction<StartableStore> {
@@ -15,6 +15,11 @@ import lombok.extern.jackson.Jacksonized;
public class StoreStopActionProvider implements HubLeafProvider<StoppableStore>, BatchHubProvider<StoppableStore> {
@Override
public boolean runParallel() {
return true;
}
@Override
public StoreActionCategory getCategory() {
return StoreActionCategory.CUSTOM;
+1 -1
View File
@@ -1964,7 +1964,7 @@ syncToPlainDirectory=Sync to plain directory
syncToPlainDirectoryDescription=When syncing to a local directory, you can either treat this directory as another git repository or just as a plain directory. If the plain directory setting is enabled, the directory is not initialized as a git repository.
openSpiceSession=Open SPICE session
terminalBehaviour=Terminal behaviour
noScanPossible=No connection types are supported by the system
noScanPossible=No supported connections were found
networkSwitchPorts=Network switch ports
nswitchGroup.displayName=Network switch ports
nswitchGroup.displayDescription=List available ports on a network switch device