From f707a0b1f5061316dcf3a0c42c2ead6ea26b5fa0 Mon Sep 17 00:00:00 2001 From: crschnick Date: Sun, 2 Jun 2024 14:07:48 +0000 Subject: [PATCH] Merge branch 'services' into release-10.0 --- .../app/comp/store/StoreCreationComp.java | 4 + .../app/comp/store/StoreCreationMenu.java | 6 +- .../xpipe/app/comp/store/StoreEntryComp.java | 3 +- .../io/xpipe/app/comp/store/StoreSection.java | 3 +- .../xpipe/app/comp/store/StoreViewState.java | 18 ++- .../io/xpipe/app/ext/DataStoreProvider.java | 5 +- .../xpipe/app/prefs/ExternalEditorType.java | 8 +- .../io/xpipe/app/storage/DataStorage.java | 2 + .../app/terminal/ExternalTerminalType.java | 41 ++--- .../xpipe/app/terminal/WezTerminalType.java | 101 +++++++++---- .../java/io/xpipe/app/util/AskpassAlert.java | 4 + .../java/io/xpipe/app/util/FileOpener.java | 11 +- .../java/io/xpipe/app/util/HostHelper.java | 8 + build.gradle | 8 +- .../process/CommandFeedbackPredicate.java | 6 + .../java/io/xpipe/core/store/LocalStore.java | 7 +- .../core/store/NetworkTunnelSession.java | 12 ++ .../xpipe/core/store/NetworkTunnelStore.java | 142 ++++++++++++++++++ .../io/xpipe/core/store/ServiceStore.java | 23 +++ .../java/io/xpipe/core/store/Session.java | 21 ++- .../io/xpipe/core/store/SessionChain.java | 45 ++++++ .../io/xpipe/core/store/SessionListener.java | 6 + .../core/store/SingletonSessionStore.java | 16 +- dist/changelogs/9.4_incremental.md | 20 ++- dist/jpackage.gradle | 2 +- .../base/service/AbstractServiceStore.java | 37 +++++ .../service/AbstractServiceStoreProvider.java | 68 +++++++++ .../ext/base/service/CustomServiceStore.java | 17 +++ .../service/CustomServiceStoreProvider.java | 70 +++++++++ .../ext/base/service/FixedServiceStore.java | 24 +++ .../service/FixedServiceStoreProvider.java | 33 ++++ .../ext/base/service/ServiceGroupStore.java | 28 ++++ .../service/ServiceGroupStoreProvider.java | 89 +++++++++++ .../ext/base/service/ServiceOpenAction.java | 70 +++++++++ ext/base/src/main/java/module-info.java | 9 +- .../img/serviceGroup_icon-16-dark.png | Bin 0 -> 728 bytes .../resources/img/serviceGroup_icon-16.png | Bin 0 -> 729 bytes .../img/serviceGroup_icon-24-dark.png | Bin 0 -> 1237 bytes .../resources/img/serviceGroup_icon-24.png | Bin 0 -> 1250 bytes .../img/serviceGroup_icon-40-dark.png | Bin 0 -> 2216 bytes .../resources/img/serviceGroup_icon-40.png | Bin 0 -> 2242 bytes .../resources/img/serviceGroup_icon-dark.svg | 127 ++++++++++++++++ .../base/resources/img/serviceGroup_icon.svg | 130 ++++++++++++++++ .../resources/img/service_icon-16-dark.png | Bin 0 -> 861 bytes .../base/resources/img/service_icon-16.png | Bin 0 -> 861 bytes .../resources/img/service_icon-24-dark.png | Bin 0 -> 1359 bytes .../base/resources/img/service_icon-24.png | Bin 0 -> 1359 bytes .../resources/img/service_icon-40-dark.png | Bin 0 -> 2454 bytes .../base/resources/img/service_icon-40.png | Bin 0 -> 2454 bytes .../base/resources/img/service_icon-dark.svg | 100 ++++++++++++ .../ext/base/resources/img/service_icon.svg | 100 ++++++++++++ gradle/gradle_scripts/vernacular-1.16.jar | Bin 101276 -> 101280 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- lang/app/strings/translations_en.properties | 1 + lang/base/strings/translations_en.properties | 11 ++ version | 2 +- 56 files changed, 1347 insertions(+), 93 deletions(-) create mode 100644 core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java create mode 100644 core/src/main/java/io/xpipe/core/store/NetworkTunnelSession.java create mode 100644 core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java create mode 100644 core/src/main/java/io/xpipe/core/store/ServiceStore.java create mode 100644 core/src/main/java/io/xpipe/core/store/SessionChain.java create mode 100644 core/src/main/java/io/xpipe/core/store/SessionListener.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStore.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStoreProvider.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16-dark.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24-dark.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40-dark.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-dark.svg create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon.svg create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16-dark.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24-dark.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40-dark.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-dark.svg create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon.svg diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java index 7220f5e8..a9588494 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java @@ -286,6 +286,10 @@ public class StoreCreationComp extends DialogComp { if (ex instanceof ValidationException) { ErrorEvent.expected(ex); skippable.set(false); + } else if (ex instanceof StackOverflowError) { + // Cycles in connection graphs can fail hard but are expected + ErrorEvent.expected(ex); + skippable.set(false); } else { skippable.set(true); } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java index cdcb3dc2..cbb597a5 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java @@ -40,12 +40,16 @@ public class StoreCreationMenu { menu.getItems() .add(category( - "addCommand", "mdi2c-code-greater-than", DataStoreProvider.CreationCategory.COMMAND, "cmd")); + "addService", "mdi2c-cloud-braces", DataStoreProvider.CreationCategory.SERVICE, null)); menu.getItems() .add(category( "addTunnel", "mdi2v-vector-polyline-plus", DataStoreProvider.CreationCategory.TUNNEL, null)); + menu.getItems() + .add(category( + "addCommand", "mdi2c-code-greater-than", DataStoreProvider.CreationCategory.COMMAND, "cmd")); + menu.getItems() .add(category("addDatabase", "mdi2d-database-plus", DataStoreProvider.CreationCategory.DATABASE, null)); } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java index da3ab6be..92a9c76b 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java @@ -410,7 +410,6 @@ public abstract class StoreEntryComp extends SimpleComp { var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); StoreViewState.get() .getSortedCategories(wrapper.getCategory().getValue().getRoot()) - .getList() .forEach(storeCategoryWrapper -> { MenuItem m = new MenuItem(); m.textProperty().setValue(" ".repeat(storeCategoryWrapper.getDepth()) + storeCategoryWrapper.getName().getValue()); @@ -418,7 +417,7 @@ public abstract class StoreEntryComp extends SimpleComp { wrapper.moveTo(storeCategoryWrapper.getCategory()); event.consume(); }); - if (storeCategoryWrapper.getParent() == null) { + if (storeCategoryWrapper.getParent() == null || storeCategoryWrapper.equals(wrapper.getCategory().getValue())) { m.setDisable(true); } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java index 67bb0cda..f44db436 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java @@ -172,7 +172,6 @@ public class StoreSection { var ordered = sorted(cached, category); var filtered = ordered.filtered( section -> { - var showFilter = filterString == null || section.matchesFilter(filterString.get()); var matchesSelector = section.anyMatches(entryFilter); // Prevent updates for children on category switching by checking depth var showCategory = category == null @@ -185,7 +184,7 @@ public class StoreSection { !DataStorage.get().isRootEntry(section.getWrapper().getEntry()); var showProvider = section.getWrapper().getEntry().getProvider() == null || section.getWrapper().getEntry().getProvider().shouldShow(section.getWrapper()); - return showFilter && matchesSelector && showCategory && notRoot && showProvider; + return matchesSelector && showCategory && notRoot && showProvider; }, category, filterString, diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java b/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java index 238cfe0d..f45f80ad 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java @@ -281,11 +281,9 @@ public class StoreViewState { public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) { var o1Root = o1.getRoot(); var o2Root = o2.getRoot(); - if (o1Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) { return -1; } - if (o2Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) { return 1; } @@ -302,6 +300,22 @@ public class StoreViewState { return 1; } + if (o1.getDepth() > o2.getDepth()) { + if (o1.getParent() == o2) { + return 1; + } + + return compare(o1.getParent(), o2); + } + + if (o1.getDepth() < o2.getDepth()) { + if (o2.getParent() == o1) { + return -1; + } + + return compare(o1, o2.getParent()); + } + var parent = compare(o1.getParent(), o2.getParent()); if (parent != 0) { return parent; diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java index b05302b0..a7cf6cb4 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -31,7 +31,9 @@ public interface DataStoreProvider { default boolean shouldShow(StoreEntryWrapper w) { return true; } - + + default void onChildrenRefresh(DataStoreEntry entry) {} + default ObservableBooleanValue busy(StoreEntryWrapper wrapper) { return new SimpleBooleanProperty(false); } @@ -216,6 +218,7 @@ public interface DataStoreProvider { HOST, DATABASE, SHELL, + SERVICE, COMMAND, TUNNEL, SCRIPT, diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java index dce1a563..d044ea8c 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -194,10 +194,10 @@ public interface ExternalEditorType extends PrefsChoiceValue { @Override public void launch(Path file) throws Exception { - ExternalApplicationHelper.startAsync(CommandBuilder.of() - .add("open", "-a") - .addQuoted(applicationName) - .addFile(file.toString())); + try (var sc = LocalShell.getShell().start()) { + sc.executeSimpleCommand(CommandBuilder.of() + .add("open", "-a").addQuoted(applicationName).addFile(file.toString())); + } } } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index dbdac50c..c68ee24f 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -446,6 +446,8 @@ public abstract class DataStorage { } }); saveAsync(); + toAdd.forEach(dataStoreEntryRef -> dataStoreEntryRef.get().getProvider().onChildrenRefresh(dataStoreEntryRef.getEntry())); + toUpdate.forEach(dataStoreEntryRef -> dataStoreEntryRef.getKey().getProvider().onChildrenRefresh(dataStoreEntryRef.getKey())); return !newChildren.isEmpty(); } diff --git a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java index 5fa67647..580cfdac 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java @@ -514,17 +514,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override public void launch(LaunchConfiguration configuration) throws Exception { - try (ShellControl pc = LocalShell.getShell()) { - var suffix = "\"" + configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"") + "\""; - pc.osascriptCommand(String.format( - """ - activate application "Terminal" - delay 1 - tell app "Terminal" to do script %s - """, - suffix)) - .execute(); - } + LocalShell.getShell() + .executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("Terminal.app") + .addFile(configuration.getScriptFile())); } }; ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") { @@ -550,26 +544,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override public void launch(LaunchConfiguration configuration) throws Exception { - try (ShellControl pc = LocalShell.getShell()) { - pc.osascriptCommand(String.format( - """ - if application "iTerm" is not running then - launch application "iTerm" - delay 1 - tell application "iTerm" - tell current tab of current window - close - end tell - end tell - end if - tell application "iTerm" - activate - create window with default profile command "%s" - end tell - """, - configuration.getScriptFile().toString().replaceAll("\"", "\\\\\""))) - .execute(); - } + LocalShell.getShell() + .executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("iTerm.app") + .addFile(configuration.getScriptFile())); } }; ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") { diff --git a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java index 8bf48d1a..f63d33be 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java @@ -1,9 +1,14 @@ package io.xpipe.app.terminal; +import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.ExternalApplicationHelper; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.LocalShell; +import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.WindowsRegistry; import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellControl; import java.nio.file.Path; import java.util.Optional; @@ -26,7 +31,7 @@ public interface WezTerminalType extends ExternalTerminalType { @Override default boolean isRecommended() { - return false; + return OsType.getLocal() != OsType.WINDOWS; } @Override @@ -51,25 +56,62 @@ public interface WezTerminalType extends ExternalTerminalType { @Override protected Optional determineInstallation() { - Optional launcherDir; - launcherDir = WindowsRegistry.local().readValue( - WindowsRegistry.HKEY_LOCAL_MACHINE, - "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BCF6F0DA-5B9A-408D-8562-F680AE6E1EAF}_is1", - "InstallLocation") - .map(p -> p + "\\wezterm-gui.exe"); - return launcherDir.map(Path::of); + try { + var foundKey = WindowsRegistry.local().findKeyForEqualValueMatchRecursive(WindowsRegistry.HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "http://wezfurlong.org/wezterm"); + if (foundKey.isPresent()) { + var installKey = WindowsRegistry.local().readValue( + foundKey.get().getHkey(), + foundKey.get().getKey(), + "InstallLocation"); + if (installKey.isPresent()) { + return installKey.map(p -> p + "\\wezterm-gui.exe").map(Path::of); + } + } + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).omit().handle(); + } + + try (ShellControl pc = LocalShell.getShell()) { + if (pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm-gui"))) { + return Optional.of(Path.of("wezterm-gui")); + } + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + } + + return Optional.empty(); } } - class Linux extends SimplePathType implements WezTerminalType { + class Linux extends ExternalApplicationType implements WezTerminalType { public Linux() { - super("app.wezterm", "wezterm-gui", true); + super("app.wezterm"); + } + + public boolean isAvailable() { + try (ShellControl pc = LocalShell.getShell()) { + return pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm")) && + pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm-gui")); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + return false; + } } @Override - protected CommandBuilder toCommand(LaunchConfiguration configuration) { - return CommandBuilder.of().add("start").addFile(configuration.getScriptFile()); + public void launch(LaunchConfiguration configuration) throws Exception { + var spawn = LocalShell.getShell().command(CommandBuilder.of().addFile("wezterm") + .add("cli", "spawn") + .addFile(configuration.getScriptFile())) + .executeAndCheck(); + if (!spawn) { + ExternalApplicationHelper.startAsync(CommandBuilder.of() + .addFile("wezterm-gui") + .add("start") + .addFile(configuration.getScriptFile())); + } } } @@ -81,20 +123,27 @@ public interface WezTerminalType extends ExternalTerminalType { @Override public void launch(LaunchConfiguration configuration) throws Exception { - var path = LocalShell.getShell() - .command(String.format( - "mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null", - applicationName)) - .readStdoutOrThrow(); - var c = CommandBuilder.of() - .addFile(Path.of(path) - .resolve("Contents") - .resolve("MacOS") - .resolve("wezterm-gui") - .toString()) - .add("start") - .add(configuration.getDialectLaunchCommand()); - ExternalApplicationHelper.startAsync(c); + try (var sc = LocalShell.getShell()) { + var path = sc.command( + String.format("mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null", + applicationName)).readStdoutOrThrow(); + var spawn = sc.command(CommandBuilder.of().addFile(Path.of(path) + .resolve("Contents") + .resolve("MacOS") + .resolve("wezterm").toString()) + .add("cli", "spawn", "--pane-id", "0") + .addFile(configuration.getScriptFile())) + .executeAndCheck(); + if (!spawn) { + ExternalApplicationHelper.startAsync(CommandBuilder.of() + .addFile(Path.of(path) + .resolve("Contents") + .resolve("MacOS") + .resolve("wezterm-gui").toString()) + .add("start") + .addFile(configuration.getScriptFile())); + } + } } } } diff --git a/app/src/main/java/io/xpipe/app/util/AskpassAlert.java b/app/src/main/java/io/xpipe/app/util/AskpassAlert.java index fee05d49..18243a54 100644 --- a/app/src/main/java/io/xpipe/app/util/AskpassAlert.java +++ b/app/src/main/java/io/xpipe/app/util/AskpassAlert.java @@ -56,6 +56,10 @@ public class AskpassAlert { @Override public void handle(long now) { + if (!stage.isShowing()) { + return; + } + if (regainedFocusCount >= 2) { return; } diff --git a/app/src/main/java/io/xpipe/app/util/FileOpener.java b/app/src/main/java/io/xpipe/app/util/FileOpener.java index 62390236..bd742a62 100644 --- a/app/src/main/java/io/xpipe/app/util/FileOpener.java +++ b/app/src/main/java/io/xpipe/app/util/FileOpener.java @@ -28,10 +28,9 @@ public class FileOpener { try { editor.launch(Path.of(localFile).toRealPath()); } catch (Exception e) { - ErrorEvent.fromThrowable(e) - .description("Unable to launch editor " + ErrorEvent.fromThrowable("Unable to launch editor " + editor.toTranslatedString().getValue() - + ".\nMaybe try to use a different editor in the settings.") + + ".\nMaybe try to use a different editor in the settings.", e) .expected() .handle(); } @@ -52,8 +51,7 @@ public class FileOpener { } } } catch (Exception e) { - ErrorEvent.fromThrowable(e) - .description("Unable to open file " + localFile) + ErrorEvent.fromThrowable("Unable to open file " + localFile, e) .handle(); } } @@ -68,8 +66,7 @@ public class FileOpener { pc.executeSimpleCommand("open \"" + localFile + "\""); } } catch (Exception e) { - ErrorEvent.fromThrowable(e) - .description("Unable to open file " + localFile) + ErrorEvent.fromThrowable("Unable to open file " + localFile, e) .handle(); } } diff --git a/app/src/main/java/io/xpipe/app/util/HostHelper.java b/app/src/main/java/io/xpipe/app/util/HostHelper.java index c73857f6..9e337ff0 100644 --- a/app/src/main/java/io/xpipe/app/util/HostHelper.java +++ b/app/src/main/java/io/xpipe/app/util/HostHelper.java @@ -6,6 +6,14 @@ import java.util.Locale; public class HostHelper { + private static int portCounter = 0; + + public static int randomPort() { + var p = 40000 + portCounter; + portCounter = portCounter + 1 % 1000; + return p; + } + public static int findRandomOpenPortOnAllLocalInterfaces() throws IOException { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); diff --git a/build.gradle b/build.gradle index f94acf1c..8baa20c6 100644 --- a/build.gradle +++ b/build.gradle @@ -88,7 +88,6 @@ project.ext { arch = getArchName() privateExtensions = file("$rootDir/private_extensions.txt").exists() ? file("$rootDir/private_extensions.txt").readLines() : [] isFullRelease = System.getenv('RELEASE') != null && Boolean.parseBoolean(System.getenv('RELEASE')) - isPreRelease = System.getenv('PRERELEASE') != null && Boolean.parseBoolean(System.getenv('PRERELEASE')) isStage = System.getenv('STAGE') != null && Boolean.parseBoolean(System.getenv('STAGE')) rawVersion = file('version').text.trim() versionString = rawVersion + (isFullRelease || isStage ? '' : '-SNAPSHOT') @@ -106,7 +105,7 @@ project.ext { website = 'https://xpipe.io' sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe' authors = 'Christopher Schnick' - javafxVersion = '22.0.1' + javafxVersion = '23-ea+18' platformName = getPlatformName() languages = ["en", "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "tr", "da"] jvmRunArgs = [ @@ -159,6 +158,11 @@ if (isFullRelease && rawVersion.contains("-")) { throw new IllegalArgumentException("Releases must have canonical versions") } + +if (isStage && !rawVersion.contains("-")) { + throw new IllegalArgumentException("Stage releases must have release numbers") +} + def replaceVariablesInFileAsString(String f, Map replacements) { def fileName = file(f).getName() def text = file(f).text diff --git a/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java b/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java new file mode 100644 index 00000000..10a80005 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java @@ -0,0 +1,6 @@ +package io.xpipe.core.process; + +public interface CommandFeedbackPredicate { + + boolean test(CommandBuilder command) throws Exception; +} diff --git a/core/src/main/java/io/xpipe/core/store/LocalStore.java b/core/src/main/java/io/xpipe/core/store/LocalStore.java index f9656abb..b76fbc1f 100644 --- a/core/src/main/java/io/xpipe/core/store/LocalStore.java +++ b/core/src/main/java/io/xpipe/core/store/LocalStore.java @@ -8,7 +8,7 @@ import io.xpipe.core.util.JacksonizedValue; import com.fasterxml.jackson.annotation.JsonTypeName; @JsonTypeName("local") -public class LocalStore extends JacksonizedValue implements ShellStore, StatefulDataStore { +public class LocalStore extends JacksonizedValue implements NetworkTunnelStore, ShellStore, StatefulDataStore { @Override public Class getStateClass() { @@ -23,4 +23,9 @@ public class LocalStore extends JacksonizedValue implements ShellStore, Stateful pc.withShellStateFail(this); return pc; } + + @Override + public DataStore getNetworkParent() { + return null; + } } diff --git a/core/src/main/java/io/xpipe/core/store/NetworkTunnelSession.java b/core/src/main/java/io/xpipe/core/store/NetworkTunnelSession.java new file mode 100644 index 00000000..126e8f4d --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/NetworkTunnelSession.java @@ -0,0 +1,12 @@ +package io.xpipe.core.store; + +public abstract class NetworkTunnelSession extends Session { + + protected NetworkTunnelSession(SessionListener listener) { + super(listener); + } + + public abstract int getLocalPort(); + + public abstract int getRemotePort(); +} diff --git a/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java b/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java new file mode 100644 index 00000000..0a5da727 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java @@ -0,0 +1,142 @@ +package io.xpipe.core.store; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public interface NetworkTunnelStore extends DataStore { + + static AtomicInteger portCounter = new AtomicInteger(); + + public static int randomPort() { + var p = 40000 + portCounter.get(); + portCounter.set(portCounter.get() + 1 % 1000); + return p; + } + + interface TunnelFunction { + + NetworkTunnelSession create(int localPort, int remotePort); + } + + DataStore getNetworkParent(); + + default boolean requiresTunnel() { + NetworkTunnelStore current = this; + while (true) { + var func = current.tunnelSession(); + if (func != null) { + return true; + } + + if (current.getNetworkParent() == null) { + return false; + } + + if (current.getNetworkParent() instanceof NetworkTunnelStore t) { + current = t; + } else { + return false; + } + } + } + + default boolean isLocallyTunneable() { + NetworkTunnelStore current = this; + while (true) { + if (current.getNetworkParent() == null) { + return true; + } + + if (current.getNetworkParent() instanceof NetworkTunnelStore t) { + current = t; + } else { + return false; + } + } + } + + default NetworkTunnelSession sessionChain(int local, int remotePort) throws Exception { + if (!isLocallyTunneable()) { + throw new IllegalStateException(); + } + + var running = new AtomicBoolean(); + var runningCounter = new AtomicInteger(); + var counter = new AtomicInteger(); + var sessions = new ArrayList(); + NetworkTunnelStore current = this; + do { + var func = current.tunnelSession(); + if (func == null) { + continue; + } + + var currentLocalPort = isLast(current) ? local : randomPort(); + var currentRemotePort = sessions.isEmpty() ? remotePort : sessions.getLast().getLocalPort(); + var t = func.create(currentLocalPort, currentRemotePort); + t.addListener(r -> { + if (r) { + runningCounter.incrementAndGet(); + } else { + runningCounter.decrementAndGet(); + } + running.set(runningCounter.get() == counter.get()); + }); + t.start(); + sessions.add(t); + counter.incrementAndGet(); + } while ((current = (NetworkTunnelStore) current.getNetworkParent()) != null); + + if (sessions.size() == 1) { + return sessions.getFirst(); + } + + if (sessions.isEmpty()) { + return new NetworkTunnelSession(null) { + + @Override + public boolean isRunning() { + return false; + } + + @Override + public void start() throws Exception { + + } + + @Override + public void stop() throws Exception { + + } + + @Override + public int getLocalPort() { + return local; + } + + @Override + public int getRemotePort() { + return remotePort; + } + }; + } + + return new SessionChain(running1 -> {}, sessions); + } + + default boolean isLast(NetworkTunnelStore tunnelStore) { + NetworkTunnelStore current = tunnelStore; + while ((current = (NetworkTunnelStore) current.getNetworkParent()) != null) { + var func = current.tunnelSession(); + if (func != null) { + return false; + } + } + return true; + } + + default TunnelFunction tunnelSession() { + return null; + } +} diff --git a/core/src/main/java/io/xpipe/core/store/ServiceStore.java b/core/src/main/java/io/xpipe/core/store/ServiceStore.java new file mode 100644 index 00000000..2763b5b3 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/ServiceStore.java @@ -0,0 +1,23 @@ +package io.xpipe.core.store; + +import java.util.OptionalInt; + +public interface ServiceStore extends SingletonSessionStore { + + NetworkTunnelStore getParent(); + + int getPort(); + + OptionalInt getTargetPort(); + + @Override + default SessionChain newSession() throws Exception { + var s = getParent().tunnelSession(); + return null; + } + + @Override + default Class getSessionClass() { + return null; + } +} diff --git a/core/src/main/java/io/xpipe/core/store/Session.java b/core/src/main/java/io/xpipe/core/store/Session.java index 5607fade..8e02e38c 100644 --- a/core/src/main/java/io/xpipe/core/store/Session.java +++ b/core/src/main/java/io/xpipe/core/store/Session.java @@ -1,10 +1,29 @@ package io.xpipe.core.store; -public abstract class Session { +public abstract class Session implements AutoCloseable { + + protected SessionListener listener; + + protected Session(SessionListener listener) { + this.listener = listener; + } + + public void addListener(SessionListener n) { + var current = this.listener; + this.listener = running -> { + current.onStateChange(running); + n.onStateChange(running); + }; + } public abstract boolean isRunning(); public abstract void start() throws Exception; public abstract void stop() throws Exception; + + @Override + public void close() throws Exception { + stop(); + } } diff --git a/core/src/main/java/io/xpipe/core/store/SessionChain.java b/core/src/main/java/io/xpipe/core/store/SessionChain.java new file mode 100644 index 00000000..2cf250b3 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/SessionChain.java @@ -0,0 +1,45 @@ +package io.xpipe.core.store; + +import java.util.List; + +public class SessionChain extends NetworkTunnelSession { + + private final List sessions; + private int runningCounter; + + public SessionChain(SessionListener listener, List sessions) { + super(listener); + this.sessions = sessions; + sessions.forEach(session -> session.addListener(running -> { + runningCounter += running ? 1 : -1; + })); + } + + public int getLocalPort() { + return sessions.getFirst().getLocalPort(); + } + + @Override + public int getRemotePort() { + return sessions.getLast().getRemotePort(); + } + + @Override + public boolean isRunning() { + return sessions.stream().allMatch(session -> session.isRunning()); + } + + @Override + public void start() throws Exception { + for (Session session : sessions) { + session.start(); + } + } + + @Override + public void stop() throws Exception { + for (Session session : sessions) { + session.stop(); + } + } +} diff --git a/core/src/main/java/io/xpipe/core/store/SessionListener.java b/core/src/main/java/io/xpipe/core/store/SessionListener.java new file mode 100644 index 00000000..45e241e1 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/SessionListener.java @@ -0,0 +1,6 @@ +package io.xpipe.core.store; + +public interface SessionListener { + + void onStateChange(boolean running); +} diff --git a/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java b/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java index 3e6a9d06..a2ac66b4 100644 --- a/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java +++ b/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java @@ -1,6 +1,7 @@ package io.xpipe.core.store; -public interface SingletonSessionStore extends ExpandedLifecycleStore, InternalCacheDataStore { +public interface SingletonSessionStore + extends ExpandedLifecycleStore, InternalCacheDataStore, SessionListener { @Override default void finalizeValidate() throws Exception { @@ -19,9 +20,10 @@ public interface SingletonSessionStore extends ExpandedLifecy return getCache("sessionEnabled", Boolean.class, false); } - default void onSessionUpdate(boolean active) { - setSessionEnabled(active); - setCache("sessionRunning", active); + @Override + default void onStateChange(boolean running) { + setSessionEnabled(running); + setCache("sessionRunning", running); } T newSession() throws Exception; @@ -50,9 +52,9 @@ public interface SingletonSessionStore extends ExpandedLifecy s = newSession(); s.start(); setCache("session", s); - onSessionUpdate(true); + onStateChange(true); } catch (Exception ex) { - onSessionUpdate(false); + onStateChange(false); throw ex; } } @@ -65,7 +67,7 @@ public interface SingletonSessionStore extends ExpandedLifecy if (ex != null) { ex.stop(); setCache("session", null); - onSessionUpdate(false); + onStateChange(false); } } } diff --git a/dist/changelogs/9.4_incremental.md b/dist/changelogs/9.4_incremental.md index 09f53bde..cc1a9e9d 100644 --- a/dist/changelogs/9.4_incremental.md +++ b/dist/changelogs/9.4_incremental.md @@ -8,6 +8,8 @@ The file transfer mechanism when editing files had some flaws, which under rare The entire transfer implementation has been rewritten to iron out these issues and increase reliability. Other file browser actions have also been made more reliable. +There seems to be another separate issue with a PowerShell bug when connecting to a Windows system, causing file uploads to be slow. For now, xpipe can fall back to pwsh if it is installed to work around this issue. + ## Git vault improvements The conflict resolution has been improved @@ -15,11 +17,27 @@ The conflict resolution has been improved - In case of a merge conflict, overwriting local changes will now preserve all connections that are not added to the git vault, including local connections - You now have the option to force push changes when a conflict occurs while XPipe is saving while running, not requiring a restart anymore +## Terminal improvements + +The terminal integration got reworked for some terminals: +- iTerm can now launch tabs instead of individual windows. There were also a few issues fixed that prevented it from launching sometimes +- WezTerm now supports tabs on Linux and macOS. The Windows installation detection has been improved to detect all installed versions +- Terminal.app will now launch faster + ## Other - You can now add simple RDP connections without a file - Fix VMware Player/Workstation and MSYS2 not being detected on Windows. Now simply searching for connections should add them automatically if they are installed - The file browser sidebar now only contains connections that can be opened in it, reducing the amount of connection shown +- Clarify error message for RealVNC servers, highlighting that RealVNC uses a proprietary protocol spec that can't be supported by third-party VNC clients like xpipe +- Fix Linux builds containing unnecessary debug symbols +- Fix AUR package also installing a debug package +- Fix application restart not working properly on macOS +- Fix possibility of selecting own children connections as hosts, causing a stack overflow. Please don't try to create cycles in your connection graphs +- Fix vault secrets not correctly updating unless restarted when changing vault passphrase +- Fix connection launcher desktop shortcuts and URLs not properly executing if xpipe is not running +- Fix move to ... menu sometimes not ordering categories correctly - Fix SSH command failing on macOS with homebrew openssh package installed -- Fix SSH connections not opening the correct shell environment on Windows when username contained spaces due to an OpenSSH bug +- Fix SSH connections not opening the correct shell environment on Windows systems when username contained spaces due to an OpenSSH bug - Fix newly added connections not having the correct order +- Fix error messages of external editor programs not being shown when they failed to start diff --git a/dist/jpackage.gradle b/dist/jpackage.gradle index b4dd6a47..62ae673d 100644 --- a/dist/jpackage.gradle +++ b/dist/jpackage.gradle @@ -58,7 +58,7 @@ jlink { ] if (org.gradle.internal.os.OperatingSystem.current().isLinux()) { - options += ['--strip-native-debug-symbols'] + options.addAll('--strip-native-debug-symbols', 'exclude-debuginfo-files') } if (useBundledJavaFx) { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java new file mode 100644 index 00000000..62f9ef53 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java @@ -0,0 +1,37 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.HostHelper; +import io.xpipe.app.util.Validators; +import io.xpipe.core.store.*; +import io.xpipe.core.util.JacksonizedValue; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@Getter +public abstract class AbstractServiceStore extends JacksonizedValue implements SingletonSessionStore, DataStore { + + public abstract DataStoreEntryRef getHost(); + + private final Integer remotePort; + private final Integer localPort; + + @Override + public void checkComplete() throws Throwable { + Validators.nonNull(getHost()); + Validators.isType(getHost(), NetworkTunnelStore.class); + Validators.nonNull(remotePort); + } + + @Override + public NetworkTunnelSession newSession() throws Exception { + var l = localPort != null ? localPort : HostHelper.findRandomOpenPortOnAllLocalInterfaces(); + return getHost().getStore().sessionChain(l, remotePort); + } + + @Override + public Class getSessionClass() { + return NetworkTunnelSession.class; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java new file mode 100644 index 00000000..21aa1c40 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java @@ -0,0 +1,68 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.comp.store.StoreEntryComp; +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.SingletonSessionStoreProvider; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.DataStoreFormatter; +import io.xpipe.core.store.DataStore; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public abstract class AbstractServiceStoreProvider implements SingletonSessionStoreProvider, DataStoreProvider { + + @Override + public DataStoreEntry getSyntheticParent(DataStoreEntry store) { + AbstractServiceStore s = store.getStore().asNeeded(); + return DataStorage.get().getOrCreateNewSyntheticEntry(s.getHost().get(), "Services", ServiceGroupStore.builder().parent(s.getHost()).build()); + } + + @Override + public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { + var toggle = createToggleComp(sec); + toggle.setCustomVisibility(Bindings.createBooleanBinding( + () -> { + AbstractServiceStore s = sec.getWrapper().getEntry().getStore().asNeeded(); + if (!s.getHost().getStore().requiresTunnel()) { + return false; + } + + return true; + }, + sec.getWrapper().getCache())); + return StoreEntryComp.create(sec.getWrapper(), toggle, preferLarge); + } + + @Override + public List getSearchableTerms(DataStore store) { + AbstractServiceStore s = store.asNeeded(); + return s.getLocalPort() != null ? List.of("" + s.getRemotePort(), "" + s.getLocalPort()) : List.of("" + s.getRemotePort()); + } + + @Override + public String summaryString(StoreEntryWrapper wrapper) { + AbstractServiceStore s = wrapper.getEntry().getStore().asNeeded(); + return DataStoreFormatter.toApostropheName(s.getHost().get()) + " service"; + } + + @Override + public ObservableValue informationString(StoreEntryWrapper wrapper) { + AbstractServiceStore s = wrapper.getEntry().getStore().asNeeded(); + if (s.getLocalPort() != null) { + return new SimpleStringProperty("Port " + s.getLocalPort() + " <- " + s.getRemotePort()); + } else { + return new SimpleStringProperty("Port " + s.getRemotePort()); + } + } + + @Override + public String getDisplayIconFileName(DataStore store) { + return "base:service_icon.svg"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java new file mode 100644 index 00000000..8893e9fa --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java @@ -0,0 +1,17 @@ +package io.xpipe.ext.base.service; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.core.store.NetworkTunnelStore; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@SuperBuilder +@Getter +@Jacksonized +@JsonTypeName("service") +public class CustomServiceStore extends AbstractServiceStore { + + private final DataStoreEntryRef host; +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java new file mode 100644 index 00000000..73a1bc95 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java @@ -0,0 +1,70 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.ext.GuiDialog; +import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.NetworkTunnelStore; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.List; + +public class CustomServiceStoreProvider extends AbstractServiceStoreProvider { + + @Override + public CreationCategory getCreationCategory() { + return CreationCategory.SERVICE; + } + + @Override + public GuiDialog guiDialog(DataStoreEntry entry, Property store) { + CustomServiceStore st = store.getValue().asNeeded(); + var host = new SimpleObjectProperty<>(st.getHost()); + var localPort = new SimpleObjectProperty<>(st.getLocalPort()); + var remotePort = new SimpleObjectProperty<>(st.getRemotePort()); + + var q = new OptionsBuilder() + .nameAndDescription("serviceHost") + .addComp( + DataStoreChoiceComp.other( + host, + NetworkTunnelStore.class, + n -> n.getStore().isLocallyTunneable(), + StoreViewState.get().getAllConnectionsCategory()), + host) + .nonNull() + .nameAndDescription("serviceRemotePort") + .addInteger(remotePort) + .nonNull() + .nameAndDescription("serviceLocalPort") + .addInteger(localPort) + .bind( + () -> { + return CustomServiceStore.builder() + .host(host.get()) + .localPort(localPort.get()) + .remotePort(remotePort.get()) + .build(); + }, + store); + return q.buildDialog(); + } + + @Override + public DataStore defaultStore() { + return CustomServiceStore.builder().build(); + } + + @Override + public List getPossibleNames() { + return List.of("service"); + } + + @Override + public List> getStoreClasses() { + return List.of(CustomServiceStore.class); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java new file mode 100644 index 00000000..4d9ee022 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java @@ -0,0 +1,24 @@ +package io.xpipe.ext.base.service; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.NetworkTunnelStore; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@SuperBuilder +@Getter +@Jacksonized +@JsonTypeName("fixedService") +public class FixedServiceStore extends AbstractServiceStore { + + private final DataStoreEntryRef host; + private final DataStoreEntryRef parent; + + @Override + public DataStoreEntryRef getHost() { + return host; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java new file mode 100644 index 00000000..2d4e78b4 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java @@ -0,0 +1,33 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.storage.DataStoreEntry; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public class FixedServiceStoreProvider extends AbstractServiceStoreProvider { + + @Override + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + FixedServiceStore s = store.getStore().asNeeded(); + return s.getParent().get(); + } + + @Override + public List getPossibleNames() { + return List.of("fixedService"); + } + + @Override + public ObservableValue informationString(StoreEntryWrapper wrapper) { + FixedServiceStore s = wrapper.getEntry().getStore().asNeeded(); + return new SimpleStringProperty("Port " + s.getRemotePort()); + } + + @Override + public List> getStoreClasses() { + return List.of(FixedServiceStore.class); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStore.java new file mode 100644 index 00000000..22c2d8a4 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStore.java @@ -0,0 +1,28 @@ +package io.xpipe.ext.base.service; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.Validators; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.util.JacksonizedValue; +import io.xpipe.ext.base.GroupStore; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@Getter +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +@SuperBuilder +@Jacksonized +@JsonTypeName("serviceGroup") +public class ServiceGroupStore extends JacksonizedValue implements DataStore, GroupStore { + + DataStoreEntryRef parent; + + @Override + public void checkComplete() throws Throwable { + Validators.nonNull(parent); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStoreProvider.java new file mode 100644 index 00000000..15e3e2dc --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStoreProvider.java @@ -0,0 +1,89 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.comp.base.StoreToggleComp; +import io.xpipe.app.comp.base.SystemStateComp; +import io.xpipe.app.comp.store.StoreEntryComp; +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.store.DataStore; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.List; + +public class ServiceGroupStoreProvider implements DataStoreProvider { + + @Override + public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { + var t = createToggleComp(sec); + return StoreEntryComp.create(sec.getWrapper(), t, preferLarge); + } + + private StoreToggleComp createToggleComp(StoreSection sec) { + var enabled = new SimpleBooleanProperty(); + var t = new StoreToggleComp(null, sec, enabled, aBoolean -> { + var children = DataStorage.get().getStoreChildren(sec.getWrapper().getEntry()); + ThreadHelper.runFailableAsync(() -> { + for (DataStoreEntry child : children) { + if (child.getStore() instanceof AbstractServiceStore serviceStore) { + if (aBoolean) { + serviceStore.startSessionIfNeeded(); + } else { + serviceStore.stopSessionIfNeeded(); + } + } + } + }); + }); + t.setCustomVisibility(Bindings.createBooleanBinding(() -> { + var children = DataStorage.get().getStoreChildren(sec.getWrapper().getEntry()); + for (DataStoreEntry child : children) { + if (child.getStore() instanceof AbstractServiceStore serviceStore) { + if (serviceStore.getHost().getStore().requiresTunnel()) { + return true; + } + } + } + return false; + }, StoreViewState.get().getAllEntries().getList())); + return t; + } + + @Override + public Comp stateDisplay(StoreEntryWrapper w) { + return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS)); + } + + @Override + public String getDisplayIconFileName(DataStore store) { + return "base:serviceGroup_icon.svg"; + } + + @Override + public DataStore defaultStore() { + return ServiceGroupStore.builder().build(); + } + + @Override + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + ServiceGroupStore s = store.getStore().asNeeded(); + return s.getParent().get(); + } + + @Override + public List getPossibleNames() { + return List.of("serviceGroup"); + } + + @Override + public List> getStoreClasses() { + return List.of(ServiceGroupStore.class); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java new file mode 100644 index 00000000..48e5084a --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java @@ -0,0 +1,70 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.Hyperlinks; +import javafx.beans.value.ObservableValue; +import lombok.Value; + +public class ServiceOpenAction implements ActionProvider { + + @Override + public String getId() { + return "openWebsite"; + } + + @Override + public ActionProvider.DataStoreCallSite getDataStoreCallSite() { + return new ActionProvider.DataStoreCallSite() { + + @Override + public boolean isMajor(DataStoreEntryRef o) { + return true; + } + + @Override + public boolean canLinkTo() { + return true; + } + + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store.getStore()); + } + + @Override + public Class getApplicableClass() { + return AbstractServiceStore.class; + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("openWebsite"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2w-web"; + } + }; + } + + @Value + static class Action implements ActionProvider.Action { + + AbstractServiceStore serviceStore; + + @Override + public boolean requiresJavaFXPlatform() { + return true; + } + + @Override + public void execute() throws Exception { + serviceStore.startSessionIfNeeded(); + var l = serviceStore.getSession().getLocalPort(); + Hyperlinks.open("http://localhost:" + l); + } + } +} diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 447e8f0a..03a689f1 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -8,6 +8,10 @@ import io.xpipe.ext.base.desktop.DesktopCommandStoreProvider; import io.xpipe.ext.base.desktop.DesktopEnvironmentStoreProvider; import io.xpipe.ext.base.script.ScriptGroupStoreProvider; import io.xpipe.ext.base.script.SimpleScriptStoreProvider; +import io.xpipe.ext.base.service.FixedServiceStoreProvider; +import io.xpipe.ext.base.service.ServiceGroupStoreProvider; +import io.xpipe.ext.base.service.ServiceOpenAction; +import io.xpipe.ext.base.service.CustomServiceStoreProvider; import io.xpipe.ext.base.store.StorePauseAction; import io.xpipe.ext.base.store.StoreStartAction; import io.xpipe.ext.base.store.StoreStopAction; @@ -18,6 +22,7 @@ open module io.xpipe.ext.base { exports io.xpipe.ext.base.script; exports io.xpipe.ext.base.store; exports io.xpipe.ext.base.desktop; + exports io.xpipe.ext.base.service; requires java.desktop; requires io.xpipe.core; @@ -55,7 +60,7 @@ open module io.xpipe.ext.base { UnzipAction, JavapAction, JarAction; - provides ActionProvider with + provides ActionProvider with ServiceOpenAction, StoreStopAction, StoreStartAction, StorePauseAction, @@ -67,7 +72,7 @@ open module io.xpipe.ext.base { EditStoreAction, DeleteStoreChildrenAction, BrowseStoreAction; - provides DataStoreProvider with + provides DataStoreProvider with ServiceGroupStoreProvider, CustomServiceStoreProvider, FixedServiceStoreProvider, SimpleScriptStoreProvider, DesktopEnvironmentStoreProvider, DesktopApplicationStoreProvider, diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..0c175e65980272b5111c8c6ec7a31cf781446d8c GIT binary patch literal 728 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 QRsaA107*qoM6N<$g3+)flmGw# literal 0 HcmV?d00001 diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-dark.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-dark.svg new file mode 100644 index 00000000..cef212aa --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-dark.svg @@ -0,0 +1,127 @@ + + + + + + + ssh draft + + + + + ssh draft + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon.svg new file mode 100644 index 00000000..d838e990 --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon.svg @@ -0,0 +1,130 @@ + + + + + + + ssh draft + + + + + ssh draft + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9bfa541ef2ce69393b4089b3c523e9cdef7e3379 GIT binary patch literal 861 zcmV-j1ETziP)3M#QsrOl*P+v>)DWGaSy z%_OZc1r>^MQJP8S-}kPI&V-3~_1@*2doJ&sC;nGM=Y|T-*kOMPxn23C%DT8XyR#M8K8XYQ^Z7`HQtm^<62>0Gj;52$Pc>H^LyUmUnCFU|`hy_# z!qlFr>gt<~=iWmmXYiz%jhmPPovyp;;vF*IHxt{@dtc^|;4hyvzAb!Rcwk#;pdY}a zFF`v)U}K78u1T0RxYydgC3cVS9 z-TqpudC!`Fh)FXG0cT#%wY%O4epf?-5@%M)hL;8FyZP00{eq#Bf^U#zoaa?wK~ZVw zeaV*1D?xU|!Kj&SHR&DT3lcl4P>a*gRKDvTm>!Lh=kBmW4Z=8j zB-iq_p07Zsrbn;MH~^O1F=02%*x0cNATG{cGUs;4csw4dY;7h!k5MwSf2zGJ$JczO z?B#6w?&b8m68jB4c4lWR23M#QsrOl*P+v>)DWGaSy z%_OZc1r>^MQJP8S-}kPI&V-3~_1@*2doJ&sC;nGM=Y|T-*kOMPxn23C%DT8XyR#M8K8XYQ^Z7`HQtm^<62>0Gj;52$Pc>H^LyUmUnCFU|`hy_# z!qlFr>gt<~=iWmmXYiz%jhmPPovyp;;vF*IHxt{@dtc^|;4hyvzAb!Rcwk#;pdY}a zFF`v)U}K78u1T0RxYydgC3cVS9 z-TqpudC!`Fh)FXG0cT#%wY%O4epf?-5@%M)hL;8FyZP00{eq#Bf^U#zoaa?wK~ZVw zeaV*1D?xU|!Kj&SHR&DT3lcl4P>a*gRKDvTm>!Lh=kBmW4Z=8j zB-iq_p07Zsrbn;MH~^O1F=02%*x0cNATG{cGUs;4csw4dY;7h!k5MwSf2zGJ$JczO z?B#6w?&b8m68jB4c4lWR2=^WA9+5h_85PzWgs z&Rk|71Pp{NDxz(t6%#}(1YAf%#JJ!Rqec@j{(u-QXb=oh69j20iDKFrKxGL_n`WplW|E{o5bGdR8NNQQnG_#y{;v5D5F`sInEP?~~I}a`>@7 zS^?a!8#Jmv0T&#S(^7r&uFB2iLKO)DQ%Ls(;JU&^fzhg39xY8&-zM@OZvzENVP=TTG4bM>Xv0#Ip)mEdE;m5F~XPr~OPOK-{9*4#RLXNn4l|Hi0OX<0YE)Z5ADvP? zye~kY*87?wYt!HWdP%IV0A^`NUXAY;-#ys~RGbY!PNz8Y4Z?JwIZ_%q3bG4iKx@B# z(LmVSVvSUG^+^%ljRfO!8%q^=1;Xw|f5^8jQ0ps8odvKXxQP-OE~2RPN2@Zg~9U zWzf5OxQsDONI#Jav>V_&A|tv|+m#K-D0{OK3o208QwnNlJY9{0m4U!@1H^#$7 zFCxW>-T7^8lM&VeLj@BGguGYt@U;3v?!JjX-*1!c3K6@rfZ$(PtFI!|P$XP|YKy`Y1XYQszjc@AsAI1*=U^&Jg@=rsh9A!(aICjwh z}4V$J+M+48W0K z(`C>XK}G;ginL~JXwt4l_^s=>Cn@SfwM5}n6;_~}QjtOtmK=NH_yx#ZIAQm7R6hZZ zqR!&<=cBhxsUF_??Fso&lW+i~$Cd{)=b<8W7oIg{oL_tk+O<+do@?{?bih R6~h1k002ovPDHLkV1m7)WDx)W literal 0 HcmV?d00001 diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24.png new file mode 100644 index 0000000000000000000000000000000000000000..74eeff8a11804d89f22cee968fdd74a35f2f0029 GIT binary patch literal 1359 zcmV-V1+e;wP)=^WA9+5h_85PzWgs z&Rk|71Pp{NDxz(t6%#}(1YAf%#JJ!Rqec@j{(u-QXb=oh69j20iDKFrKxGL_n`WplW|E{o5bGdR8NNQQnG_#y{;v5D5F`sInEP?~~I}a`>@7 zS^?a!8#Jmv0T&#S(^7r&uFB2iLKO)DQ%Ls(;JU&^fzhg39xY8&-zM@OZvzENVP=TTG4bM>Xv0#Ip)mEdE;m5F~XPr~OPOK-{9*4#RLXNn4l|Hi0OX<0YE)Z5ADvP? zye~kY*87?wYt!HWdP%IV0A^`NUXAY;-#ys~RGbY!PNz8Y4Z?JwIZ_%q3bG4iKx@B# z(LmVSVvSUG^+^%ljRfO!8%q^=1;Xw|f5^8jQ0ps8odvKXxQP-OE~2RPN2@Zg~9U zWzf5OxQsDONI#Jav>V_&A|tv|+m#K-D0{OK3o208QwnNlJY9{0m4U!@1H^#$7 zFCxW>-T7^8lM&VeLj@BGguGYt@U;3v?!JjX-*1!c3K6@rfZ$(PtFI!|P$XP|YKy`Y1XYQszjc@AsAI1*=U^&Jg@=rsh9A!(aICjwh z}4V$J+M+48W0K z(`C>XK}G;ginL~JXwt4l_^s=>Cn@SfwM5}n6;_~}QjtOtmK=NH_yx#ZIAQm7R6hZZ zqR!&<=cBhxsUF_??Fso&lW+i~$Cd{)=b<8W7oIg{oL_tk+O<+do@?{?bih R6~h1k002ovPDHLkV1m7)WDx)W literal 0 HcmV?d00001 diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..fcf4083c21d94a076e8f37e3127254e79720f6e9 GIT binary patch literal 2454 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 + + + + + + ssh draft + + + + ssh draft + + + + + + + + + + + + + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon.svg new file mode 100644 index 00000000..f13ea71f --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon.svg @@ -0,0 +1,100 @@ + + + + + + + ssh draft + + + + ssh draft + + + + + + + + + + + + + + + + diff --git a/gradle/gradle_scripts/vernacular-1.16.jar b/gradle/gradle_scripts/vernacular-1.16.jar index 062d3b270b4f227b4998849ea7d8c2abf2cf031f..261aaa8fb4cdb4e45a33ea9258abe3ddd944b739 100644 GIT binary patch delta 4470 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 RK(nEw3@A#LMIob%{{!Tp9F+h7 delta 4573 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 510f28ae..6f7a6eb3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-rc-1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/lang/app/strings/translations_en.properties b/lang/app/strings/translations_en.properties index 6d5fbbda..026f8491 100644 --- a/lang/app/strings/translations_en.properties +++ b/lang/app/strings/translations_en.properties @@ -25,6 +25,7 @@ moveTo=Move to ... addDatabase=Database ... browseInternalStorage=Browse internal storage addTunnel=Tunnel ... +addService=Service ... addScript=Script ... addHost=Remote Host ... addShell=Shell Environment ... diff --git a/lang/base/strings/translations_en.properties b/lang/base/strings/translations_en.properties index 5f12de7a..819cf9ec 100644 --- a/lang/base/strings/translations_en.properties +++ b/lang/base/strings/translations_en.properties @@ -132,5 +132,16 @@ desktopCommand.displayName=Desktop command desktopCommand.displayDescription=Run a command in a remote desktop environment desktopCommandScript=Commands desktopCommandScriptDescription=The commands to run in the environment +service.displayName=Service +service.displayDescription=Forward a remote service to your local machine +serviceLocalPort=Explicit local port +serviceLocalPortDescription=The local port to forward to, otherwise a random one is used +serviceRemotePort=Remote port +serviceRemotePortDescription=The port on which the service is running on +serviceHost=Service host +serviceHostDescription=The host the service is running on +openWebsite=Open website +serviceGroup.displayName=Service group +serviceGroup.displayDescription=Group multiple services into one category diff --git a/version b/version index cdebdcf6..0359f243 100644 --- a/version +++ b/version @@ -1 +1 @@ -9.4-3 +9.4