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 00000000..0c175e65 Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16.png new file mode 100644 index 00000000..1f2adcbd Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24-dark.png new file mode 100644 index 00000000..ee50e717 Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24.png new file mode 100644 index 00000000..6bf7ae2a Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40-dark.png new file mode 100644 index 00000000..2c97a68b Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40.png new file mode 100644 index 00000000..4a2d36ef Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40.png differ 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 00000000..9bfa541e Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16.png new file mode 100644 index 00000000..9bfa541e Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24-dark.png new file mode 100644 index 00000000..74eeff8a Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24-dark.png differ 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 00000000..74eeff8a Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24.png differ 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 00000000..fcf4083c Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40.png new file mode 100644 index 00000000..fcf4083c Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-dark.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-dark.svg new file mode 100644 index 00000000..9ef40b7c --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-dark.svg @@ -0,0 +1,100 @@ + + + + + + + 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 062d3b27..261aaa8f 100644 Binary files a/gradle/gradle_scripts/vernacular-1.16.jar and b/gradle/gradle_scripts/vernacular-1.16.jar differ 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