From 5556e3483f9ec17095c3c4cd9f451bd76815bf9a Mon Sep 17 00:00:00 2001 From: crschnick Date: Wed, 8 Feb 2023 21:34:19 +0000 Subject: [PATCH] Restructure proc module --- README.md | 16 +- app/build.gradle | 2 +- app/src/main/java/io/xpipe/app/core/App.java | 4 +- .../xpipe/app/launcher/LauncherCommand.java | 4 +- .../app/prefs/ExternalApplicationType.java | 2 +- .../xpipe/app/prefs/ExternalEditorType.java | 58 +- .../io/xpipe/app/storage/DataStoreEntry.java | 1 + .../io/xpipe/app/storage/StandardStorage.java | 45 +- .../io/xpipe/app/update/AppInstaller.java | 4 +- .../java/io/xpipe/app/update/AppUpdater.java | 2 +- .../io/xpipe/core/charsetter/Charsetter.java | 4 +- .../java/io/xpipe/core/impl/LocalStore.java | 3 +- .../core/impl/ProcessControlProvider.java | 50 - .../java/io/xpipe/core/process/OsType.java | 6 +- .../core/process/ProcessControlProvider.java | 54 +- .../core/process/ShellProcessControl.java | 3 + .../io/xpipe/core/process/ShellTypes.java | 19 +- .../java/io/xpipe/core/store/ShellStore.java | 29 +- .../io/xpipe/core/util/XPipeInstallation.java | 12 +- .../xpipe/core/util/XPipeTempDirectory.java | 2 +- core/src/main/java/module-info.java | 2 +- .../ext/pdx/parser/TextFormatParser.java | 7 +- ext/proc/build.gradle | 39 + .../io/xpipe/ext/proc/CommandControlImpl.java | 241 ++ .../xpipe/ext/proc/ExternalTerminalType.java | 288 ++ .../ext/proc/LocalCommandControlImpl.java | 87 + .../xpipe/ext/proc/LocalShellControlImpl.java | 187 + .../java/io/xpipe/ext/proc/ProcPrefs.java | 71 + .../java/io/xpipe/ext/proc/ProcProvider.java | 38 + .../io/xpipe/ext/proc/ProcessControlImpl.java | 47 + .../io/xpipe/ext/proc/ShellControlImpl.java | 118 + .../xpipe/ext/proc/TerminalProviderImpl.java | 13 + .../proc/action/InstallConnectorAction.java | 64 + .../xpipe/ext/proc/action/LaunchAction.java | 89 + .../ext/proc/action/LaunchShortcutAction.java | 67 + .../proc/augment/CmdCommandAugmentation.java | 25 + .../ext/proc/augment/CommandAugmentation.java | 70 + .../proc/augment/NoCommandAugmentation.java | 20 + .../PosixShellCommandAugmentation.java | 29 + .../PowershellCommandAugmentation.java | 28 + .../proc/augment/SshCommandAugmentation.java | 32 + .../io/xpipe/ext/proc/store/CommandStore.java | 89 + .../ext/proc/store/CommandStoreProvider.java | 159 + .../io/xpipe/ext/proc/store/DockerStore.java | 69 + .../ext/proc/store/DockerStoreProvider.java | 98 + .../ext/proc/store/ShellCommandStore.java | 51 + .../proc/store/ShellCommandStoreProvider.java | 113 + .../ext/proc/store/ShellEnvironmentStore.java | 54 + .../store/ShellEnvironmentStoreProvider.java | 102 + .../ext/proc/store/ShellTypeChoiceComp.java | 66 + .../io/xpipe/ext/proc/store/SshStore.java | 64 + .../ext/proc/store/SshStoreProvider.java | 162 + .../io/xpipe/ext/proc/store/WslStore.java | 111 + .../ext/proc/store/WslStoreProvider.java | 118 + ext/proc/src/main/java/module-info.java | 50 + ....xpipe.core.process.ProcessControlProvider | 1 + .../ext/proc/resources/extension.properties | 1 + .../xpipe/ext/proc/resources/img/cmd_icon.png | Bin 0 -> 3004 bytes .../proc/resources/img/defaultShell_icon.png | Bin 0 -> 1686 bytes .../ext/proc/resources/img/docker_icon.png | Bin 0 -> 6820 bytes .../proc/resources/img/shellCommand_icon.png | Bin 0 -> 6376 bytes .../resources/img/shellEnvironment_icon.png | Bin 0 -> 5500 bytes .../ext/proc/resources/img/sink_icon.png | Bin 0 -> 6624 bytes .../xpipe/ext/proc/resources/img/ssh_icon.png | Bin 0 -> 14502 bytes .../xpipe/ext/proc/resources/img/wsl_icon.png | Bin 0 -> 21115 bytes .../xpipe/ext/proc/resources/img/wsl_icon.svg | 3409 +++++++++++++++++ .../resources/lang/translations_de.properties | 3 + .../resources/lang/translations_en.properties | 96 + ext/proc/src/test/java/module-info.java | 12 + .../src/test/java/test/CommandStoreTest.java | 64 + ext/proc/src/test/java/test/CommandTests.java | 95 + ext/proc/src/test/java/test/FailureTests.java | 83 + ext/proc/src/test/java/test/FileTest.java | 56 + ext/proc/src/test/java/test/ShellTests.java | 80 + .../java/test/item/BasicShellTestItem.java | 34 + .../java/test/item/CommandCheckTestItem.java | 30 + .../java/test/item/ShellCheckTestItem.java | 112 + .../test/java/test/item/ShellTestItem.java | 14 + ext/proc/src/test/resources/utf8-bom-lf.txt | 2 + ext/procx/build.gradle | 1 + ext/procx/src/main/java/module-info.java | 1 + .../io/xpipe/extension/DataStoreProvider.java | 2 +- .../extension/XPipeServiceProviders.java | 2 +- .../extension/util/XPipeDistributionType.java | 2 +- 84 files changed, 7118 insertions(+), 170 deletions(-) delete mode 100644 core/src/main/java/io/xpipe/core/impl/ProcessControlProvider.java create mode 100644 ext/proc/build.gradle create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/CommandControlImpl.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/ExternalTerminalType.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/LocalCommandControlImpl.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/LocalShellControlImpl.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/ProcPrefs.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/ProcProvider.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/ProcessControlImpl.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/ShellControlImpl.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/TerminalProviderImpl.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/action/InstallConnectorAction.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/action/LaunchAction.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/action/LaunchShortcutAction.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/augment/CmdCommandAugmentation.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/augment/CommandAugmentation.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/augment/NoCommandAugmentation.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/augment/PosixShellCommandAugmentation.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/augment/PowershellCommandAugmentation.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/augment/SshCommandAugmentation.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/CommandStore.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/CommandStoreProvider.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/DockerStore.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/DockerStoreProvider.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellCommandStore.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellCommandStoreProvider.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellEnvironmentStore.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellEnvironmentStoreProvider.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellTypeChoiceComp.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/SshStore.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/SshStoreProvider.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/WslStore.java create mode 100644 ext/proc/src/main/java/io/xpipe/ext/proc/store/WslStoreProvider.java create mode 100644 ext/proc/src/main/java/module-info.java create mode 100644 ext/proc/src/main/resources/META-INF/services/io.xpipe.core.process.ProcessControlProvider create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/extension.properties create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/cmd_icon.png create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/defaultShell_icon.png create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/docker_icon.png create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/shellCommand_icon.png create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/shellEnvironment_icon.png create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/sink_icon.png create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/ssh_icon.png create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/wsl_icon.png create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/wsl_icon.svg create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/lang/translations_de.properties create mode 100644 ext/proc/src/main/resources/io/xpipe/ext/proc/resources/lang/translations_en.properties create mode 100644 ext/proc/src/test/java/module-info.java create mode 100644 ext/proc/src/test/java/test/CommandStoreTest.java create mode 100644 ext/proc/src/test/java/test/CommandTests.java create mode 100644 ext/proc/src/test/java/test/FailureTests.java create mode 100644 ext/proc/src/test/java/test/FileTest.java create mode 100644 ext/proc/src/test/java/test/ShellTests.java create mode 100644 ext/proc/src/test/java/test/item/BasicShellTestItem.java create mode 100644 ext/proc/src/test/java/test/item/CommandCheckTestItem.java create mode 100644 ext/proc/src/test/java/test/item/ShellCheckTestItem.java create mode 100644 ext/proc/src/test/java/test/item/ShellTestItem.java create mode 100644 ext/proc/src/test/resources/utf8-bom-lf.txt create mode 100644 ext/procx/build.gradle create mode 100644 ext/procx/src/main/java/module-info.java diff --git a/README.md b/README.md index f57b7069..b5abc3f0 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,22 @@ The other modules make up the X-Pipe implementation and are licensed under GPL: - [app](app) - Contains the X-Pipe daemon implementation and the X-Pipe desktop application code - [cli](cli) - The X-Pipe CLI implementation, a GraalVM native image application - [dist](dist) - Tools to create a distributable package of X-Pipe -- [ext](ext) - Available X-Pipe extensions. Note that essentially every feature is implemented as an extension +- [ext](ext) - Available X-Pipe extensions. Essentially every feature is implemented as an extension +### Open source model + +X-Pipe utilizes an open core model, which essentially means that +the main application core is open source while certain other components are not. +In this case these non open source components are planned to be future parts of a potential commercialization. +Furthermore, some tests and especially test environments and that run on private servers +are also not included in this repository (Don't want to leak server information). +Finally, scripts and workflows to create signed executables and installers +are also not included to prevent attackers from easily impersonating the shipping the X-Pipe application malware. + +The license model is chosen in such a way that you are +able to use and integrate X-Pipe within your application through the MIT-licensed API. +In any other case where you plan to contribute to the X-Pipe platform itself, which is GPL licensed, +I would still have to figure out how to exactly handle these kinds of contributions. ## Development diff --git a/app/build.gradle b/app/build.gradle index 288f3dfb..d036cc89 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,7 +89,7 @@ List jvmRunArgs = [ "--add-exports", "org.apache.commons.lang3/org.apache.commons.lang3.math=io.xpipe.extension", "--add-opens", "java.base/java.lang.reflect=com.jfoenix", "--add-opens", "java.base/java.lang.reflect=com.jfoenix", - "--add-opens", "java.base/java.lang=io.xpipe.app", + "--add-opens", "java.base/java.lang=io.xpipe.core", "--add-opens", "com.dustinredmond.fxtrayicon/com.dustinredmond.fxtrayicon=io.xpipe.app", "--add-opens", "net.synedra.validatorfx/net.synedra.validatorfx=io.xpipe.extension", "-Xmx8g", diff --git a/app/src/main/java/io/xpipe/app/core/App.java b/app/src/main/java/io/xpipe/app/core/App.java index 1fe5e704..18266b46 100644 --- a/app/src/main/java/io/xpipe/app/core/App.java +++ b/app/src/main/java/io/xpipe/app/core/App.java @@ -2,6 +2,7 @@ package io.xpipe.app.core; import io.xpipe.app.Main; import io.xpipe.app.comp.AppLayoutComp; +import io.xpipe.core.process.OsType; import io.xpipe.extension.event.ErrorEvent; import io.xpipe.extension.event.TrackEvent; import io.xpipe.extension.fxcomps.util.PlatformThread; @@ -10,7 +11,6 @@ import javafx.application.Platform; import javafx.scene.image.Image; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; -import org.apache.commons.lang3.SystemUtils; import javax.imageio.ImageIO; import java.awt.*; @@ -38,7 +38,7 @@ public class App extends Application { // Set dock icon explicitly on mac // This is necessary in case X-Pipe was started through a script as it will have no icon otherwise - if (SystemUtils.IS_OS_MAC) { + if (OsType.getLocal().equals(OsType.MACOS)) { try { var iconUrl = Main.class.getResourceAsStream("resources/img/logo.png"); if (iconUrl != null) { diff --git a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java index 6f6391ff..184d0138 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java @@ -77,7 +77,7 @@ public class LauncherCommand implements Callable { OpenExchange.Request.builder().arguments(inputs).build()); } - if (OsType.getLocal().equals(OsType.MAC)) { + if (OsType.getLocal().equals(OsType.MACOS)) { Desktop.getDesktop().setOpenURIHandler(e -> { con.performSimpleExchange( OpenExchange.Request.builder().arguments(List.of(e.getURI().toString())).build()); @@ -120,7 +120,7 @@ public class LauncherCommand implements Callable { LauncherInput.handle(inputs); // URL open operations have to be handled in a special way on macOS! - if (OsType.getLocal().equals(OsType.MAC)) { + if (OsType.getLocal().equals(OsType.MACOS)) { Desktop.getDesktop().setOpenURIHandler(e -> { LauncherInput.handle(List.of(e.getURI().toString())); }); diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java index cb3b843e..bcb6713e 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java @@ -57,7 +57,7 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { @Override public boolean isSelectable() { - return OsType.getLocal().equals(OsType.MAC); + return OsType.getLocal().equals(OsType.MACOS); } @Override 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 b86b5e38..5c8028a2 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -5,7 +5,6 @@ import io.xpipe.core.process.ShellTypes; import io.xpipe.extension.prefs.PrefsChoiceValue; import io.xpipe.extension.util.ApplicationHelper; import io.xpipe.extension.util.WindowsRegistry; -import org.apache.commons.lang3.SystemUtils; import java.io.IOException; import java.nio.file.Path; @@ -38,13 +37,9 @@ public interface ExternalEditorType extends PrefsChoiceValue { @Override protected Optional determinePath() { - Optional launcherDir = Optional.empty(); - if (SystemUtils.IS_OS_WINDOWS) { - launcherDir = WindowsRegistry.readString( - WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Notepad++", null) - .map(p -> p + "\\notepad++.exe"); - } - + Optional launcherDir; + launcherDir = WindowsRegistry.readString(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Notepad++", null) + .map(p -> p + "\\notepad++.exe"); return launcherDir.map(Path::of); } }; @@ -71,7 +66,8 @@ public interface ExternalEditorType extends PrefsChoiceValue { @Override public void launch(Path file) throws Exception { - ApplicationHelper.executeLocalApplication(List.of("open", "-a", getApplicationPath().orElseThrow().toString(), file.toString())); + ApplicationHelper.executeLocalApplication( + List.of("open", "-a", getApplicationPath().orElseThrow().toString(), file.toString())); } } @@ -92,7 +88,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { var format = customCommand.contains("$file") ? customCommand : customCommand + " $file"; var fileString = file.toString().contains(" ") ? "\"" + file + "\"" : file.toString(); - ApplicationHelper.executeLocalApplication(format.replace("$file",fileString)); + ApplicationHelper.executeLocalApplication(format.replace("$file", fileString)); } @Override @@ -108,7 +104,6 @@ public interface ExternalEditorType extends PrefsChoiceValue { public void launch(Path file) throws Exception; - public static class LinuxPathType extends ExternalApplicationType.PathApplication implements ExternalEditorType { public LinuxPathType(String id, String command) { @@ -127,7 +122,8 @@ public interface ExternalEditorType extends PrefsChoiceValue { } } - public abstract static class WindowsFullPathType extends ExternalApplicationType.WindowsFullPathType implements ExternalEditorType { + public abstract static class WindowsFullPathType extends ExternalApplicationType.WindowsFullPathType + implements ExternalEditorType { public WindowsFullPathType(String id) { super(id); @@ -147,23 +143,23 @@ public interface ExternalEditorType extends PrefsChoiceValue { public static final List WINDOWS_EDITORS = List.of(VSCODE, NOTEPADPLUSPLUS_WINDOWS, NOTEPAD); public static final List LINUX_EDITORS = List.of(VSCODE_LINUX, NOTEPADPLUSPLUS_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD); - public static final List MACOS_EDITORS = - List.of(VSCODE_MACOS, SUBLIME_MACOS, TEXT_EDIT); + public static final List MACOS_EDITORS = List.of(VSCODE_MACOS, SUBLIME_MACOS, TEXT_EDIT); public static final List ALL = ((Supplier>) () -> { - var all = new ArrayList(); - if (OsType.getLocal().equals(OsType.WINDOWS)) { - all.addAll(WINDOWS_EDITORS); - } - if (OsType.getLocal().equals(OsType.LINUX)) { - all.addAll(LINUX_EDITORS); - } - if (OsType.getLocal().equals(OsType.MAC)) { - all.addAll(MACOS_EDITORS); - } - all.add(CUSTOM); - return all; - }).get(); + var all = new ArrayList(); + if (OsType.getLocal().equals(OsType.WINDOWS)) { + all.addAll(WINDOWS_EDITORS); + } + if (OsType.getLocal().equals(OsType.LINUX)) { + all.addAll(LINUX_EDITORS); + } + if (OsType.getLocal().equals(OsType.MACOS)) { + all.addAll(MACOS_EDITORS); + } + all.add(CUSTOM); + return all; + }) + .get(); public static void detectDefault() { var typeProperty = AppPrefs.get().externalEditor; @@ -190,13 +186,13 @@ public interface ExternalEditorType extends PrefsChoiceValue { } } else { typeProperty.set(LINUX_EDITORS.stream() - .filter(externalEditorType -> externalEditorType.isAvailable()) - .findFirst() - .orElse(null)); + .filter(externalEditorType -> externalEditorType.isAvailable()) + .findFirst() + .orElse(null)); } } - if (OsType.getLocal().equals(OsType.MAC)) { + if (OsType.getLocal().equals(OsType.MACOS)) { typeProperty.set(MACOS_EDITORS.stream() .filter(externalEditorType -> externalEditorType.isAvailable()) .findFirst() diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index f493384b..cbd7ca18 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -121,6 +121,7 @@ public class DataStoreEntry extends StorageElement { var information = Optional.ofNullable(json.get("information")) .map(JsonNode::textValue) .orElse(null); + var lastUsed = Instant.parse(json.required("lastUsed").textValue()); var lastModified = Instant.parse(json.required("lastModified").textValue()); var configuration = Optional.ofNullable(json.get("configuration")) diff --git a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java index 5670ae24..77610060 100644 --- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java @@ -1,16 +1,14 @@ package io.xpipe.app.storage; -import io.xpipe.core.util.XPipeTempDirectory; +import io.xpipe.core.util.XPipeSession; import io.xpipe.extension.event.ErrorEvent; import io.xpipe.extension.event.TrackEvent; import lombok.NonNull; import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.SystemUtils; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -22,44 +20,7 @@ public class StandardStorage extends DataStorage { private DataSourceCollection recovery; private boolean isNewSession() { - try { - if (SystemUtils.IS_OS_WINDOWS) { - var sessionFile = dir.resolve("session"); - if (!Files.exists(sessionFile)) { - return true; - } - - var lastSessionEndTime = Instant.parse(Files.readString(sessionFile)); - var pf = Path.of("C:\\pagefile.sys"); - var lastBootTime = Files.getLastModifiedTime(pf).toInstant(); - return lastSessionEndTime.isBefore(lastBootTime); - } else { - var sessionFile = XPipeTempDirectory.getLocal().resolve("xpipe_session"); - return !Files.exists(sessionFile); - } - - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).omitted(true).build().handle(); - return true; - } - } - - private void writeSessionInfo() { - try { - if (SystemUtils.IS_OS_WINDOWS) { - var sessionFile = dir.resolve("session"); - var now = Instant.now().toString(); - Files.writeString(sessionFile, now); - } else { - var sessionFile = XPipeTempDirectory.getLocal().resolve("xpipe_session"); - if (!Files.exists(sessionFile)) { - Files.createFile(sessionFile); - } - } - - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).omitted(true).build().handle(); - } + return XPipeSession.get().isNewSystemSession(); } private void deleteLeftovers() { @@ -322,8 +283,6 @@ public class StandardStorage extends DataStorage { } deleteLeftovers(); - - writeSessionInfo(); } @Override diff --git a/app/src/main/java/io/xpipe/app/update/AppInstaller.java b/app/src/main/java/io/xpipe/app/update/AppInstaller.java index 9b062db9..04c381c1 100644 --- a/app/src/main/java/io/xpipe/app/update/AppInstaller.java +++ b/app/src/main/java/io/xpipe/app/update/AppInstaller.java @@ -63,7 +63,7 @@ public class AppInstaller { return Files.exists(Path.of("/etc/debian_version")) ? new InstallerAssetType.Debian() : new InstallerAssetType.Rpm(); } - if (OsType.getLocal().equals(OsType.MAC)) { + if (OsType.getLocal().equals(OsType.MACOS)) { return new InstallerAssetType.Pkg(); } @@ -82,7 +82,7 @@ public class AppInstaller { } } - if (p.getOsType().equals(OsType.MAC)) { + if (p.getOsType().equals(OsType.MACOS)) { return new InstallerAssetType.Pkg(); } diff --git a/app/src/main/java/io/xpipe/app/update/AppUpdater.java b/app/src/main/java/io/xpipe/app/update/AppUpdater.java index 9d3aa848..f2ac8cff 100644 --- a/app/src/main/java/io/xpipe/app/update/AppUpdater.java +++ b/app/src/main/java/io/xpipe/app/update/AppUpdater.java @@ -6,7 +6,7 @@ import io.xpipe.app.core.AppExtensionManager; import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.core.impl.ProcessControlProvider; +import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.util.XPipeSession; import io.xpipe.extension.event.ErrorEvent; import io.xpipe.extension.event.TrackEvent; diff --git a/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java b/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java index 87ad202a..b3159f52 100644 --- a/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java +++ b/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java @@ -112,9 +112,7 @@ public abstract class Charsetter { if (store instanceof FileStore fileStore && fileStore.getFileSystem() instanceof MachineStore m) { if (result.getNewLine() == null) { - try (var pc = m.create().start()) { - result = new Result(result.getCharset(), pc.getShellType().getNewLine()); - } + result = new Result(result.getCharset(), m.getShellType() != null ? m.getShellType().getNewLine() : null); } } diff --git a/core/src/main/java/io/xpipe/core/impl/LocalStore.java b/core/src/main/java/io/xpipe/core/impl/LocalStore.java index 2ebc9732..c70afc33 100644 --- a/core/src/main/java/io/xpipe/core/impl/LocalStore.java +++ b/core/src/main/java/io/xpipe/core/impl/LocalStore.java @@ -1,6 +1,7 @@ package io.xpipe.core.impl; import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.process.ShellProcessControl; import io.xpipe.core.store.FileSystemStore; import io.xpipe.core.store.MachineStore; @@ -47,7 +48,7 @@ public class LocalStore extends JacksonizedValue implements FileSystemStore, Mac } @Override - public ShellProcessControl create() { + public ShellProcessControl createControl() { return ProcessControlProvider.createLocal(); } diff --git a/core/src/main/java/io/xpipe/core/impl/ProcessControlProvider.java b/core/src/main/java/io/xpipe/core/impl/ProcessControlProvider.java deleted file mode 100644 index 1eff1f09..00000000 --- a/core/src/main/java/io/xpipe/core/impl/ProcessControlProvider.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.xpipe.core.impl; - -import io.xpipe.core.process.CommandProcessControl; -import io.xpipe.core.process.ShellProcessControl; -import lombok.NonNull; - -import java.util.List; -import java.util.ServiceLoader; -import java.util.function.BiFunction; -import java.util.function.Function; - -public abstract class ProcessControlProvider { - - private static List INSTANCES; - - public static void init(ModuleLayer layer) { - INSTANCES = ServiceLoader.load(layer, ProcessControlProvider.class) - .stream().map(localProcessControlProviderProvider -> localProcessControlProviderProvider.get()).toList(); - } - - public static ShellProcessControl createLocal() { - return INSTANCES.stream().map(localProcessControlProvider -> localProcessControlProvider.createLocalProcessControl()).findFirst().orElseThrow(); - } - - public static ShellProcessControl createSub( - ShellProcessControl parent, - @NonNull Function commandFunction, - BiFunction terminalCommand) { - return INSTANCES.stream().map(localProcessControlProvider -> localProcessControlProvider.sub(parent, commandFunction, terminalCommand)).findFirst().orElseThrow(); - } - - public static CommandProcessControl createCommand( - ShellProcessControl parent, - @NonNull Function command, - Function terminalCommand) { - return INSTANCES.stream().map(localProcessControlProvider -> localProcessControlProvider.command(parent, command, terminalCommand)).findFirst().orElseThrow(); - } - - public abstract ShellProcessControl sub( - ShellProcessControl parent, - @NonNull Function commandFunction, - BiFunction terminalCommand); - - public abstract CommandProcessControl command( - ShellProcessControl parent, - @NonNull Function command, - Function terminalCommand); - - public abstract ShellProcessControl createLocalProcessControl(); -} diff --git a/core/src/main/java/io/xpipe/core/process/OsType.java b/core/src/main/java/io/xpipe/core/process/OsType.java index e99b6334..236beb61 100644 --- a/core/src/main/java/io/xpipe/core/process/OsType.java +++ b/core/src/main/java/io/xpipe/core/process/OsType.java @@ -7,12 +7,12 @@ public interface OsType { Windows WINDOWS = new Windows(); Linux LINUX = new Linux(); - Mac MAC = new Mac(); + MacOs MACOS = new MacOs(); public static OsType getLocal() { String osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH); if ((osName.contains("mac")) || (osName.contains("darwin"))) { - return MAC; + return MACOS; } else if (osName.contains("win")) { return WINDOWS; } else if (osName.contains("nux")) { @@ -124,7 +124,7 @@ public interface OsType { } } - static class Mac implements OsType { + static class MacOs implements OsType { @Override public String getTempDirectory(ShellProcessControl pc) throws Exception { diff --git a/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java b/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java index 0fbee11b..903ecaa9 100644 --- a/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java +++ b/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java @@ -1,6 +1,58 @@ package io.xpipe.core.process; +import lombok.NonNull; + +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.function.BiFunction; +import java.util.function.Function; + public abstract class ProcessControlProvider { - public abstract ProcessControl local(); + private static List INSTANCES; + + public static void init(ModuleLayer layer) { + INSTANCES = ServiceLoader.load(layer, ProcessControlProvider.class) + .stream().map(localProcessControlProviderProvider -> localProcessControlProviderProvider.get()).toList(); + } + + public static ShellProcessControl createLocal() { + return INSTANCES.stream().map(localProcessControlProvider -> localProcessControlProvider.createLocalProcessControl()).findFirst().orElseThrow(); + } + + public static ShellProcessControl createSub( + ShellProcessControl parent, + @NonNull Function commandFunction, + BiFunction terminalCommand) { + return INSTANCES.stream().map(localProcessControlProvider -> localProcessControlProvider.sub(parent, commandFunction, terminalCommand)).filter( + Objects::nonNull).findFirst().orElseThrow(); + } + + public static CommandProcessControl createCommand( + ShellProcessControl parent, + @NonNull Function command, + Function terminalCommand) { + return INSTANCES.stream().map(localProcessControlProvider -> localProcessControlProvider.command(parent, command, terminalCommand)).filter( + Objects::nonNull).findFirst().orElseThrow(); + } + + public static ShellProcessControl createSsh(Object sshStore) { + return INSTANCES.stream().map(localProcessControlProvider -> localProcessControlProvider.createSshControl(sshStore)).filter( + Objects::nonNull).findFirst().orElseThrow(); + } + + public abstract ShellProcessControl sub( + ShellProcessControl parent, + @NonNull Function commandFunction, + BiFunction terminalCommand); + + public abstract CommandProcessControl command( + ShellProcessControl parent, + @NonNull Function command, + Function terminalCommand); + + public abstract ShellProcessControl createLocalProcessControl(); + + public abstract ShellProcessControl createSshControl(Object sshStore); } diff --git a/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java b/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java index 5e38444a..3e789fa8 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java @@ -6,11 +6,14 @@ import lombok.NonNull; import java.io.IOException; import java.util.List; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; public interface ShellProcessControl extends ProcessControl { + void onInit(Consumer pc); + String prepareTerminalOpen() throws Exception; String prepareIntermediateTerminalOpen(String content) throws Exception; diff --git a/core/src/main/java/io/xpipe/core/process/ShellTypes.java b/core/src/main/java/io/xpipe/core/process/ShellTypes.java index 02772ffb..ca194efe 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellTypes.java +++ b/core/src/main/java/io/xpipe/core/process/ShellTypes.java @@ -199,18 +199,15 @@ public class ShellTypes { @Override public Charset determineCharset(ShellProcessControl control) throws Exception { control.writeLine("chcp"); - + var pattern = Pattern.compile("^[\\w ]+: (\\d+)$"); var r = new BufferedReader(new InputStreamReader(control.getStdout(), StandardCharsets.US_ASCII)); - // Read echo of command - r.readLine(); - // Read actual output - var line = r.readLine(); - // Read additional empty line - r.readLine(); - - var matcher = Pattern.compile("\\d+").matcher(line); - matcher.find(); - return Charset.forName("ibm" + matcher.group()); + while (true) { + var line = r.readLine(); + var matcher = pattern.matcher(line); + if (matcher.matches()) { + return Charset.forName("ibm" + matcher.group(1)); + } + } } @Override diff --git a/core/src/main/java/io/xpipe/core/store/ShellStore.java b/core/src/main/java/io/xpipe/core/store/ShellStore.java index 3cebb016..38088551 100644 --- a/core/src/main/java/io/xpipe/core/store/ShellStore.java +++ b/core/src/main/java/io/xpipe/core/store/ShellStore.java @@ -2,10 +2,14 @@ package io.xpipe.core.store; import io.xpipe.core.charsetter.Charsetter; import io.xpipe.core.impl.LocalStore; +import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellProcessControl; import io.xpipe.core.process.ShellType; -public interface ShellStore extends DataStore { +import java.nio.charset.Charset; + +public interface ShellStore extends DataStore, StatefulDataStore +{ public static MachineStore local() { return new LocalStore(); @@ -21,7 +25,28 @@ public interface ShellStore extends DataStore { return s instanceof LocalStore; } - ShellProcessControl create(); + default ShellProcessControl create() { + var pc = createControl(); + pc.onInit(processControl -> { + setState("type", processControl.getShellType()); + setState("os", processControl.getOsType()); + setState("charset", processControl.getCharset()); + }); + return pc; + } + + default ShellType getShellType() { + return getState("type", ShellType.class, null); + } + + default OsType getOsType() { + return getState("os", OsType.class, null); + } + default Charset getCharset() { + return getState("charset", Charset.class, null); + } + + ShellProcessControl createControl(); public default ShellType determineType() throws Exception { try (var pc = create().start()) { diff --git a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java index 0442fa7a..c6eda0cb 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java @@ -20,7 +20,7 @@ public class XPipeInstallation { if (OsType.getLocal().equals(OsType.LINUX)) { return "nohup \"" + installationBase + "/app/bin/xpiped\" --mode " + mode.getDisplayName() + suffix + " & disown"; - } else if (OsType.getLocal().equals(OsType.MAC)) { + } else if (OsType.getLocal().equals(OsType.MACOS)) { return "open \"" + installationBase + "\" --args --mode " + mode.getDisplayName() + suffix; } @@ -55,7 +55,7 @@ public class XPipeInstallation { public static boolean isInstallationDistribution() { var base = getLocalInstallationBasePath(); - if (OsType.getLocal().equals(OsType.MAC)) { + if (OsType.getLocal().equals(OsType.MACOS)) { if (!base.toString().equals(getLocalDefaultInstallationBasePath(false))) { return false; } @@ -93,13 +93,13 @@ public class XPipeInstallation { public static Path getLocalExtensionsDirectory() { Path path = getLocalInstallationBasePath(); - return OsType.getLocal().equals(OsType.MAC) + return OsType.getLocal().equals(OsType.MACOS) ? path.resolve("Contents").resolve("Resources").resolve("extensions") : path.resolve("app").resolve("extensions"); } private static Path getLocalInstallationBasePathForJavaExecutable(Path executable) { - if (OsType.getLocal().equals(OsType.MAC)) { + if (OsType.getLocal().equals(OsType.MACOS)) { return executable .getParent() .getParent() @@ -115,7 +115,7 @@ public class XPipeInstallation { } private static Path getLocalInstallationBasePathForDaemonExecutable(Path executable) { - if (OsType.getLocal().equals(OsType.MAC)) { + if (OsType.getLocal().equals(OsType.MACOS)) { return executable.getParent().getParent().getParent(); } else if (OsType.getLocal().equals(OsType.LINUX)) { return executable.getParent().getParent().getParent(); @@ -138,7 +138,7 @@ public class XPipeInstallation { return defaultInstallation; } - if (OsType.getLocal().equals(OsType.MAC)) { + if (OsType.getLocal().equals(OsType.MACOS)) { return FileNames.getParent(FileNames.getParent(FileNames.getParent(cliExecutable))); } else { return FileNames.getParent(FileNames.getParent(cliExecutable)); diff --git a/core/src/main/java/io/xpipe/core/util/XPipeTempDirectory.java b/core/src/main/java/io/xpipe/core/util/XPipeTempDirectory.java index 0d1f3309..6f9eb2fa 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeTempDirectory.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeTempDirectory.java @@ -26,7 +26,7 @@ public class XPipeTempDirectory { if (!proc.executeBooleanSimpleCommand(proc.getShellType().getFileExistsCommand(dir))) { proc.executeSimpleCommand(proc.getShellType().flatten(proc.getShellType().getMkdirsCommand(dir)), "Unable to access or create temporary directory " + dir); - if (proc.getOsType().equals(OsType.LINUX) || proc.getOsType().equals(OsType.MAC)) { + if (proc.getOsType().equals(OsType.LINUX) || proc.getOsType().equals(OsType.MACOS)) { proc.executeSimpleCommand("chmod -f 777 \"" + dir + "\""); } } diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 4caf8088..c2d408bf 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -1,4 +1,4 @@ -import io.xpipe.core.impl.ProcessControlProvider; +import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.source.WriteMode; import io.xpipe.core.util.CoreJacksonModule; diff --git a/ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/TextFormatParser.java b/ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/TextFormatParser.java index 9a8c48de..f3f436f3 100644 --- a/ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/TextFormatParser.java +++ b/ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/TextFormatParser.java @@ -3,7 +3,6 @@ package io.xpipe.ext.pdx.parser; import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.data.node.TupleNode; import io.xpipe.core.data.node.ValueNode; -import org.apache.commons.lang3.SystemUtils; import java.io.IOException; import java.nio.charset.Charset; @@ -40,7 +39,7 @@ public final class TextFormatParser { public static TextFormatParser eu4() { return new TextFormatParser( - SystemUtils.IS_OS_MAC ? StandardCharsets.UTF_8 : Charset.forName("windows-1252"), + Charset.forName("windows-1252"), TaggedNodes.NO_TAGS, s -> s.equals("map_area_data")); } @@ -59,14 +58,14 @@ public final class TextFormatParser { public static TextFormatParser ck2() { return new TextFormatParser( - SystemUtils.IS_OS_MAC ? StandardCharsets.UTF_8 : Charset.forName("windows-1252"), + Charset.forName("windows-1252"), TaggedNodes.NO_TAGS, s -> false); } public static TextFormatParser vic2() { return new TextFormatParser( - SystemUtils.IS_OS_MAC ? StandardCharsets.UTF_8 : Charset.forName("windows-1252"), + Charset.forName("windows-1252"), TaggedNodes.NO_TAGS, s -> false); } diff --git a/ext/proc/build.gradle b/ext/proc/build.gradle new file mode 100644 index 00000000..00c3d687 --- /dev/null +++ b/ext/proc/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'java' + id "org.moditect.gradleplugin" version "1.0.0-rc3" +} + +apply from: "$rootDir/gradle/gradle_scripts/java.gradle" +apply from: "$rootDir/gradle/gradle_scripts/commons.gradle" +apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle" +apply from: "$rootDir/gradle/gradle_scripts/extension.gradle" + +tasks.withType(JavaCompile).configureEach { + doFirst { + options.compilerArgs += [ + '--module-path', classpath.asPath + ] + classpath = files() + } +} + +configurations { + compileOnly.extendsFrom(dep) +} + +dependencies { + compileOnly project(':app') + testImplementation project(':app') + testImplementation 'org.apache.commons:commons-lang3:3.12.0' + implementation 'org.apache.commons:commons-exec:1.3' + compileOnly group: 'com.dlsc.preferencesfx', name: 'preferencesfx-core', version: '11.15.0' + compileOnly group: 'com.dlsc.formsfx', name: 'formsfx-core', version: '11.6.0' +} + +List jvmDevArgs = [ + "--add-reads", "io.xpipe.ext.proc=ALL-UNNAMED" +] + +tasks.withType(Test) { + jvmArgs += jvmDevArgs +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/CommandControlImpl.java b/ext/proc/src/main/java/io/xpipe/ext/proc/CommandControlImpl.java new file mode 100644 index 00000000..9f3fe628 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/CommandControlImpl.java @@ -0,0 +1,241 @@ +package io.xpipe.ext.proc; + +import io.xpipe.core.process.CommandProcessControl; +import io.xpipe.core.process.ProcessOutputException; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.process.ShellType; +import io.xpipe.extension.event.TrackEvent; +import lombok.NonNull; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; + +public abstract class CommandControlImpl extends ProcessControlImpl implements CommandProcessControl { + + private static final ExecutorService stdoutReader = Executors.newFixedThreadPool(1, new ThreadFactory() { + public Thread newThread(Runnable r) { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setDaemon(true); + t.setName("stdout reader"); + return t; + } + }); + private static final ExecutorService stderrReader = Executors.newFixedThreadPool(1, new ThreadFactory() { + public Thread newThread(Runnable r) { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setDaemon(true); + t.setName("stderr reader"); + return t; + } + }); + + protected final ShellProcessControl parent; + @NonNull + protected final Function command; + protected final Function terminalCommand; + protected boolean elevated; + protected int exitCode = -1; + protected String timedOutError; + protected Integer exitTimeout; + protected boolean complex; + protected boolean obeysReturnValueConvention = true; + + public CommandControlImpl( + ShellProcessControl parent, + @NonNull Function command, + Function terminalCommand) { + this.command = command; + this.parent = parent; + this.terminalCommand = terminalCommand; + } + + @Override + public CommandProcessControl doesNotObeyReturnValueConvention() { + this.obeysReturnValueConvention = false; + return this; + } + + @Override + public CommandProcessControl sensitive() { + this.sensitive = true; + return this; + } + + public int getExitCode() { + if (running) { + waitFor(); + } + + return exitCode; + } + + @Override + public CommandProcessControl exitTimeout(Integer timeout) { + this.exitTimeout = timeout; + return this; + } + + @Override + public CommandProcessControl customCharset(Charset charset) { + this.charset = charset; + return this; + } + + @Override + public CommandProcessControl elevated() { + this.elevated = true; + return this; + } + + @Override + public ShellType getShellType() { + return parent.getShellType(); + } + + @Override + public CommandProcessControl complex() { + this.complex = true; + return this; + } + + public void discardOut() { + stdoutReader.submit(() -> { + try { + var read = new String(getStdout().readAllBytes(), getCharset()); + TrackEvent.withTrace("proc", "Discarding stdout") + .tag("output", read) + .handle(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + public void discardErr() { + stderrReader.submit(() -> { + try { + var read = new String(getStderr().readAllBytes(), getCharset()); + TrackEvent.withTrace("proc", "Discarding stderr") + .tag("output", read) + .handle(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + public String readOnlyStdout() throws Exception { + discardErr(); + var bytes = getStdout().readAllBytes(); + var string = new String(bytes, getCharset()); + TrackEvent.withTrace("proc", "Read stdout").tag("output", string).handle(); + return string.trim(); + } + + @Override + public void accumulateStdout(Consumer con) { + stderrReader.submit(() -> { + try { + var out = new String(getStdout().readAllBytes(), getCharset()).strip(); + TrackEvent.withTrace("proc", "Read stdout").tag("output", out).handle(); + con.accept(out); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + @Override + public void accumulateStderr(Consumer con) { + stderrReader.submit(() -> { + try { + var err = new String(getStderr().readAllBytes(), getCharset()).strip(); + TrackEvent.withTrace("proc", "Read stderr").tag("output", err).handle(); + con.accept(err); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + public String readOrThrow() throws Exception { + AtomicReference read = new AtomicReference<>(""); + stdoutReader.submit(() -> { + try { + var bytes = getStdout().readAllBytes(); + read.set(new String(bytes, getCharset())); + TrackEvent.withTrace("proc", "Read stdout") + .tag("output", read.get()) + .handle(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + + AtomicReference readError = new AtomicReference<>(""); + stderrReader.submit(() -> { + try { + readError.set(new String(getStderr().readAllBytes(), getCharset())); + TrackEvent.withTrace("proc", "Read stderr") + .tag("output", readError.get()) + .handle(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + + var ec = waitFor(); + if (!ec) { + throw new ProcessOutputException("Command timed out" + (timedOutError != null ? ": " + timedOutError : "")); + } + + var exitCode = getExitCode(); + var success = (obeysReturnValueConvention && exitCode == 0) || (!obeysReturnValueConvention && !(read.get().isEmpty() && !readError.get().isEmpty())); + if (success) { + return read.get().trim(); + } else { + throw new ProcessOutputException( + "Command returned with " + exitCode + ": " + readError.get().trim()); + } + } + + public void discardOrThrow() throws Exception { + AtomicReference read = new AtomicReference<>(""); + stdoutReader.submit(() -> { + try { + getStdout().transferTo(OutputStream.nullOutputStream()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + + AtomicReference readError = new AtomicReference<>(""); + stderrReader.submit(() -> { + try { + readError.set(new String(getStderr().readAllBytes(), getCharset())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + + var ec = waitFor(); + if (!ec) { + throw new ProcessOutputException("Command timed out" + (timedOutError != null ? ": " + timedOutError : "")); + } + + var exitCode = getExitCode(); + var success = (obeysReturnValueConvention && exitCode == 0) || (!obeysReturnValueConvention && !(read.get().isEmpty() && !readError.get().isEmpty())); + if (!success) { + throw new ProcessOutputException( + "Command returned with " + exitCode + ": " + readError.get().trim()); + } + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/ExternalTerminalType.java b/ext/proc/src/main/java/io/xpipe/ext/proc/ExternalTerminalType.java new file mode 100644 index 00000000..ad43175e --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/ExternalTerminalType.java @@ -0,0 +1,288 @@ +package io.xpipe.ext.proc; + +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.store.ShellStore; +import io.xpipe.extension.event.ErrorEvent; +import io.xpipe.extension.prefs.PrefsChoiceValue; +import io.xpipe.extension.prefs.PrefsProvider; +import io.xpipe.extension.util.ApplicationHelper; + +import java.util.List; + +public interface ExternalTerminalType extends PrefsChoiceValue { + + public static final ExternalTerminalType CMD = new SimpleType("proc.cmd", "cmd", "cmd.exe") { + + @Override + protected String toCommand(String name, String command) { + return "cmd.exe /C " + command; + } + + @Override + public boolean isSelectable() { + return OsType.getLocal().equals(OsType.WINDOWS); + } + }; + + public static final ExternalTerminalType POWERSHELL = + new SimpleType("proc.powershell", "powershell", "PowerShell") { + + @Override + protected String toCommand(String name, String command) { + return "powershell.exe -Command " + command; + } + + @Override + public boolean isSelectable() { + return OsType.getLocal().equals(OsType.WINDOWS); + } + }; + + public static final ExternalTerminalType WINDOWS_TERMINAL = + new SimpleType("proc.windowsTerminal", "wt.exe", "Windows Terminal") { + + @Override + protected String toCommand(String name, String command) { + return "-w 1 nt --title \"" + name + "\" " + command; + } + + @Override + public boolean isSelectable() { + return OsType.getLocal().equals(OsType.WINDOWS); + } + }; + + public static final ExternalTerminalType GNOME_TERMINAL = + new SimpleType("proc.gnomeTerminal", "gnome-terminal", "Gnome Terminal") { + + @Override + protected String toCommand(String name, String command) { + return "--title \"" + name + "\" -- " + command; + } + + @Override + public boolean isSelectable() { + return OsType.getLocal().equals(OsType.LINUX); + } + }; + + public static final ExternalTerminalType KONSOLE = new SimpleType("proc.konsole", "konsole", "Konsole") { + + @Override + protected String toCommand(String name, String command) { + return "--new-tab -e bash -c " + command; + } + + @Override + public boolean isSelectable() { + return OsType.getLocal().equals(OsType.LINUX); + } + }; + + public static final ExternalTerminalType XFCE = new SimpleType("proc.xfce", "xfce4-terminal", "Xfce") { + + @Override + protected String toCommand(String name, String command) { + return "--tab --title \"" + name + "\" --command " + command; + } + + @Override + public boolean isSelectable() { + return OsType.getLocal().equals(OsType.LINUX); + } + }; + + public static final ExternalTerminalType MACOS_TERMINAL = new MacOsTerminalType(); + + public static final ExternalTerminalType ITERM2 = new ITerm2Type(); + + public static final ExternalTerminalType WARP = new WarpType(); + + public static final ExternalTerminalType CUSTOM = new CustomType(); + + public static final List ALL = List.of( + WINDOWS_TERMINAL, + POWERSHELL, + CMD, + KONSOLE, + XFCE, + GNOME_TERMINAL, + WARP, + ITERM2, + MACOS_TERMINAL, + CUSTOM) + .stream() + .filter(terminalType -> terminalType.isSelectable()) + .toList(); + + public static ExternalTerminalType getDefault() { + return ALL.stream() + .filter(terminalType -> terminalType.isAvailable()) + .findFirst() + .orElse(null); + } + + public abstract void launch(String name, String command) throws Exception; + + static class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { + + public MacOsTerminalType() { + super("proc.macosTerminal", "Terminal"); + } + + @Override + public void launch(String name, String command) throws Exception { + try (ShellProcessControl pc = ShellStore.local().create().start()) { + var suffix = command.equals(pc.getShellType().getNormalOpenCommand()) + ? "\"\"" + : "\"" + command.replaceAll("\"", "\\\\\"") + "\""; + var cmd = "osascript -e 'tell app \"" + "Terminal" + "\" to do script " + suffix + "'"; + pc.executeSimpleCommand(cmd); + } + } + } + + static class CustomType extends ExternalApplicationType implements ExternalTerminalType { + + public CustomType() { + super("proc.custom"); + } + + @Override + public void launch(String name, String command) throws Exception { + var custom = + PrefsProvider.get(ProcPrefs.class).customTerminalCommand().getValue(); + if (custom == null || custom.trim().isEmpty()) { + return; + } + + var format = custom.contains("$cmd") ? custom : custom + " $cmd"; + try (var pc = ShellStore.local().create().start()) { + var toExecute = format.replace("$cmd", command); + if (pc.getOsType().equals(OsType.WINDOWS)) { + toExecute = "start \"" + name + "\" " + toExecute; + } else { + toExecute = "nohup " + toExecute + " /dev/null & disown"; + } + pc.executeSimpleCommand(toExecute); + } + } + + @Override + public boolean isSelectable() { + return true; + } + + @Override + public boolean isAvailable() { + return true; + } + } + + static class ITerm2Type extends ExternalApplicationType.MacApplication implements ExternalTerminalType { + + public ITerm2Type() { + super("proc.iterm2", "iTerm2"); + } + + @Override + public void launch(String name, String command) throws Exception { + try (ShellProcessControl pc = ShellStore.local().create().start()) { + var cmd = String.format( + """ + osascript - "$@" </dev/null & disown"; + } + pc.executeSimpleCommand(toExecute); + } + } + + protected abstract String toCommand(String name, String command); + + public boolean isAvailable() { + try (ShellProcessControl pc = ShellStore.local().create().start()) { + return pc.executeBooleanSimpleCommand(pc.getShellType().getWhichCommand(executable)); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + return false; + } + } + + @Override + public boolean isSelectable() { + return true; + } + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/LocalCommandControlImpl.java b/ext/proc/src/main/java/io/xpipe/ext/proc/LocalCommandControlImpl.java new file mode 100644 index 00000000..ad001e53 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/LocalCommandControlImpl.java @@ -0,0 +1,87 @@ +package io.xpipe.ext.proc; + +import io.xpipe.core.process.CommandProcessControl; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.extension.util.ScriptHelper; +import lombok.NonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +public class LocalCommandControlImpl extends CommandControlImpl { + + private Process process; + + public LocalCommandControlImpl( + ShellProcessControl parent, + @NonNull Function command, + Function terminalCommand + ) { + super(parent, command, terminalCommand); + } + + @Override + public boolean waitFor() { + try { + return process.waitFor(exitTimeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + return false; + } + } + + @Override + public String prepareTerminalOpen() throws Exception { + try (var ignored = parent.start()) { + var operator = parent.getShellType().getConcatenationOperator(); + var consoleCommand = terminalCommand.apply(parent) + + operator + + parent.getShellType().getPauseCommand(); + return parent.prepareIntermediateTerminalOpen(consoleCommand); + } + } + + @Override + public void closeStdin() throws IOException { + process.getOutputStream().close(); + } + + @Override + public boolean isStdinClosed() { + return false; + } + + @Override + public void close() throws IOException { + waitFor(); + } + + @Override + public void kill() throws Exception { + process.destroyForcibly(); + } + + @Override + public CommandProcessControl start() throws Exception { + var file = ScriptHelper.createLocalExecScript(command.apply(parent)); + process = new ProcessBuilder(parent.getShellType().executeCommandListWithShell(file)).start(); + return this; + } + + @Override + public InputStream getStdout() { + return process.getInputStream(); + } + + @Override + public OutputStream getStdin() { + return process.getOutputStream(); + } + + @Override + public InputStream getStderr() { + return process.getErrorStream(); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/LocalShellControlImpl.java b/ext/proc/src/main/java/io/xpipe/ext/proc/LocalShellControlImpl.java new file mode 100644 index 00000000..f0da5c6d --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/LocalShellControlImpl.java @@ -0,0 +1,187 @@ +package io.xpipe.ext.proc; + +import io.xpipe.core.process.ProcessControlProvider; +import io.xpipe.core.process.*; +import io.xpipe.extension.event.TrackEvent; +import io.xpipe.extension.prefs.PrefsProvider; +import io.xpipe.extension.util.ScriptHelper; +import org.apache.commons.exec.CommandLine; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; + +public class LocalShellControlImpl extends ShellControlImpl { + + private static final int EXIT_TIMEOUT = 5000; + protected boolean stdinClosed; + private Process process; + + @Override + public CommandProcessControl command( + Function command, Function terminalCommand) { + var control = ProcessControlProvider.createCommand(this, command, terminalCommand); + if (control != null) { + return control; + } + + return new LocalCommandControlImpl(this, command, terminalCommand); + } + + @Override + public String prepareTerminalOpen() throws Exception { + return prepareIntermediateTerminalOpen(null); + } + + public void closeStdin() throws IOException { + if (stdinClosed) { + return; + } + + stdinClosed = true; + getStdin().close(); + } + + @Override + public boolean isStdinClosed() { + return stdinClosed; + } + + @Override + public ShellType getShellType() { + return ShellTypes.getPlatformDefault(); + } + + @Override + public void close() throws IOException { + TrackEvent.withTrace("proc", "Closing local shell ...").handle(); + exitAndWait(); + } + + @Override + public void exitAndWait() throws IOException { + if (!running) { + return; + } + + if (!isStdinClosed()) { + writeLine(shellType.getExitCommand()); + } + + getStdout().close(); + getStderr().close(); + getStdin().close(); + + stdinClosed = true; + uuid = null; + if (!PrefsProvider.get(ProcPrefs.class).enableCaching().get()) { + shellType = null; + charset = null; + command = null; + tempDirectory = null; + } + + try { + process.waitFor(EXIT_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (InterruptedException ignored) { + } + + running = false; + } + + @Override + public void kill() throws IOException { + TrackEvent.withTrace("proc", "Killing local shell ...").handle(); + + process.destroyForcibly(); + // Don't close stout as that might hang too in case it is frozen + // getStdout().close(); + // getStderr().close(); + getStdin().close(); + + running = false; + } + + public void restart() throws Exception { + close(); + start(); + } + + @Override + public String prepareIntermediateTerminalOpen(String content) throws Exception { + try (var pc = start()) { + var initCommand = ScriptHelper.constructOpenWithInitScriptCommand(pc, initCommands, content); + TrackEvent.withDebug("proc", "Writing open init script") + .tag("initCommand", initCommand) + .tag("content", content) + .handle(); + return initCommand; + } + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public ShellProcessControl elevated(Predicate elevationFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public ShellProcessControl start() throws Exception { + if (running) { + return this; + } + + var localType = ShellTypes.getPlatformDefault(); + command = localType.getNormalOpenCommand(); + uuid = UUID.randomUUID(); + + TrackEvent.withTrace("proc", "Starting local process") + .tag("command", command) + .handle(); + var parsed = CommandLine.parse(command); + var args = new ArrayList(); + args.add(parsed.getExecutable()); + args.addAll(List.of(parsed.getArguments())); + process = Runtime.getRuntime().exec(args.toArray(String[]::new)); + stdinClosed = false; + running = true; + shellType = localType; + if (charset == null) { + charset = shellType.determineCharset(this); + } + osType = OsType.getLocal(); + + for (String s : initCommands) { + executeLine(s); + } + onInit.accept(this); + + return this; + } + + @Override + public InputStream getStdout() { + return process.getInputStream(); + } + + @Override + public OutputStream getStdin() { + return process.getOutputStream(); + } + + @Override + public InputStream getStderr() { + return process.getErrorStream(); + } + +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/ProcPrefs.java b/ext/proc/src/main/java/io/xpipe/ext/proc/ProcPrefs.java new file mode 100644 index 00000000..413189fc --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/ProcPrefs.java @@ -0,0 +1,71 @@ +package io.xpipe.ext.proc; + +import com.dlsc.formsfx.model.structure.Field; +import com.dlsc.formsfx.model.structure.SingleSelectionField; +import com.dlsc.formsfx.model.structure.StringField; +import com.dlsc.preferencesfx.formsfx.view.controls.SimpleTextControl; +import com.dlsc.preferencesfx.model.Setting; +import com.dlsc.preferencesfx.util.VisibilityProperty; +import io.xpipe.app.prefs.TranslatableComboBoxControl; +import io.xpipe.extension.prefs.PrefsChoiceValue; +import io.xpipe.extension.prefs.PrefsHandler; +import io.xpipe.extension.prefs.PrefsProvider; +import javafx.beans.property.*; +import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; + +import java.util.List; + +public class ProcPrefs extends PrefsProvider { + + private final BooleanProperty enableCaching = new SimpleBooleanProperty(true); + + public ObservableBooleanValue enableCaching() { + return enableCaching; + } + + private final ObjectProperty terminalType = new SimpleObjectProperty<>(); + private final SimpleListProperty terminalTypeList = new SimpleListProperty<>( + FXCollections.observableArrayList(PrefsChoiceValue.getSupported(ExternalTerminalType.class))); + private final SingleSelectionField terminalTypeControl = Field.ofSingleSelectionType( + terminalTypeList, terminalType) + .render(() -> new TranslatableComboBoxControl<>()); + + // Custom terminal + // =============== + private final StringProperty customTerminalCommand = new SimpleStringProperty(""); + private final StringField customTerminalCommandControl = editable( + StringField.ofStringType(customTerminalCommand).render(() -> new SimpleTextControl()), + terminalType.isEqualTo(ExternalTerminalType.CUSTOM)); + + public ObservableValue terminalType() { + return terminalType; + } + + public ObservableValue customTerminalCommand() { + return customTerminalCommand; + } + + @Override + public void addPrefs(PrefsHandler handler) { + handler.addSetting( + List.of("integrations"), + "proc.terminal", + Setting.of("app.defaultProgram", terminalTypeControl, terminalType), + ExternalTerminalType.class); + handler.addSetting( + List.of("integrations"), + "proc.terminal", + Setting.of("proc.customTerminalCommand", customTerminalCommandControl, customTerminalCommand) + .applyVisibility(VisibilityProperty.of(terminalType.isEqualTo(ExternalTerminalType.CUSTOM))), + String.class); + } + + @Override + public void init() { + if (terminalType.get() == null) { + terminalType.set(ExternalTerminalType.getDefault()); + } + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/ProcProvider.java b/ext/proc/src/main/java/io/xpipe/ext/proc/ProcProvider.java new file mode 100644 index 00000000..15ebbca6 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/ProcProvider.java @@ -0,0 +1,38 @@ +package io.xpipe.ext.proc; + +import io.xpipe.core.process.ProcessControlProvider; +import io.xpipe.core.process.CommandProcessControl; +import io.xpipe.core.process.ShellProcessControl; +import lombok.NonNull; + +import java.util.function.BiFunction; +import java.util.function.Function; + +public class ProcProvider extends ProcessControlProvider { + + @Override + public ShellProcessControl sub( + ShellProcessControl parent, @NonNull Function commandFunction, + BiFunction terminalCommand + ) { + return null; + } + + @Override + public CommandProcessControl command( + ShellProcessControl parent, @NonNull Function command, + Function terminalCommand + ) { + return null; + } + + @Override + public ShellProcessControl createLocalProcessControl() { + return new LocalShellControlImpl(); + } + + @Override + public ShellProcessControl createSshControl(Object sshStore) { + return null; + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/ProcessControlImpl.java b/ext/proc/src/main/java/io/xpipe/ext/proc/ProcessControlImpl.java new file mode 100644 index 00000000..6812a78a --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/ProcessControlImpl.java @@ -0,0 +1,47 @@ +package io.xpipe.ext.proc; + +import io.xpipe.core.process.ProcessControl; +import io.xpipe.extension.event.TrackEvent; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public abstract class ProcessControlImpl implements ProcessControl { + + protected Charset charset; + protected boolean running; + protected boolean sensitive; + + @Override + public boolean isRunning() { + return running; + } + + @Override + public void writeLine(String line) throws IOException { + if (isStdinClosed()) { + throw new IllegalStateException("Input is closed"); + } + + // Censor actual written line to prevent leaking sensitive information + TrackEvent.withTrace("proc", "Writing line").tag("line", line).handle(); + getStdin().write((line + "\n").getBytes(getCharset())); + getStdin().flush(); + } + + @Override + public void write(byte[] b) throws IOException { + if (isStdinClosed()) { + throw new IllegalStateException("Input is closed"); + } + + getStdin().write(b); + getStdin().flush(); + } + + @Override + public final Charset getCharset() { + return charset != null ? charset : StandardCharsets.US_ASCII; + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/ShellControlImpl.java b/ext/proc/src/main/java/io/xpipe/ext/proc/ShellControlImpl.java new file mode 100644 index 00000000..7f9e0016 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/ShellControlImpl.java @@ -0,0 +1,118 @@ +package io.xpipe.ext.proc; + +import io.xpipe.core.process.ProcessControlProvider; +import io.xpipe.core.process.CommandProcessControl; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.process.ShellType; +import io.xpipe.core.util.SecretValue; +import io.xpipe.core.util.XPipeTempDirectory; +import lombok.Getter; +import lombok.NonNull; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +public abstract class ShellControlImpl extends ProcessControlImpl implements ShellProcessControl { + + protected Integer startTimeout = 10000; + protected UUID uuid; + protected String command; + protected List initCommands = new ArrayList<>(); + protected String tempDirectory; + protected Consumer onInit = processControl -> {}; + + @Override + public void onInit(Consumer pc) { + this.onInit = pc; + } + + @Getter + protected ShellType shellType; + + @Getter + protected OsType osType; + + @Getter + protected SecretValue elevationPassword; + + @Override + public String getTemporaryDirectory() throws Exception { + if (tempDirectory == null) { + checkRunning(); + tempDirectory = XPipeTempDirectory.get(this); + } + + return tempDirectory; + } + + @Override + public void checkRunning() throws Exception { + if (!isRunning()) { + throw new IllegalStateException("Shell process control is not running"); + } + } + + @Override + public ShellProcessControl sensitive() { + this.sensitive = true; + return this; + } + + @Override + public ShellProcessControl elevation(SecretValue value) { + this.elevationPassword = value; + return this; + } + + @Override + public ShellProcessControl initWith(List cmds) { + this.initCommands.addAll(cmds); + return this; + } + + @Override + public ShellProcessControl subShell( + @NonNull Function command, + BiFunction terminalCommand) { + return ProcessControlProvider.createSub(this, command, terminalCommand); + } + + @Override + public CommandProcessControl command(Function command) { + return command(command, command); + } + + @Override + public CommandProcessControl command( + Function command, Function terminalCommand) { + var control = ProcessControlProvider.createCommand(this, command, terminalCommand); + return control; + } + + @Override + public void executeLine(String command) throws Exception { + writeLine(command); + if (getShellType().doesRepeatInput()) { + while (true) { + int c = getStdout().read(); + + if (c == -1) { + break; + } + + if (c == '\n') { + break; + } + } + } + } + + @Override + public abstract void exitAndWait() throws IOException; +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/TerminalProviderImpl.java b/ext/proc/src/main/java/io/xpipe/ext/proc/TerminalProviderImpl.java new file mode 100644 index 00000000..8e15f3f3 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/TerminalProviderImpl.java @@ -0,0 +1,13 @@ +package io.xpipe.ext.proc; + +import io.xpipe.app.util.TerminalProvider; +import io.xpipe.extension.prefs.PrefsProvider; + +public class TerminalProviderImpl extends TerminalProvider { + + @Override + public void openInTerminal(String title, String command) throws Exception { + var type = PrefsProvider.get(ProcPrefs.class).terminalType().getValue(); + type.launch(title, command); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/action/InstallConnectorAction.java b/ext/proc/src/main/java/io/xpipe/ext/proc/action/InstallConnectorAction.java new file mode 100644 index 00000000..fe10aac7 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/action/InstallConnectorAction.java @@ -0,0 +1,64 @@ +package io.xpipe.ext.proc.action; + +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.util.ProxyManagerProvider; +import io.xpipe.extension.I18n; +import io.xpipe.extension.util.ActionProvider; +import javafx.beans.value.ObservableValue; +import lombok.Value; + +public class InstallConnectorAction implements ActionProvider { + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry entry; + + @Override + public boolean requiresPlatform() { + return true; + } + + @Override + public void execute() throws Exception { + try (ShellProcessControl s = ((ShellStore) entry.getStore()).create().start()) { + ProxyManagerProvider.get().setup(s); + } + } + } + + @Override + public DataStoreCallSite getDataStoreCallSite() { + return new DataStoreCallSite() { + + @Override + public LaunchShortcutAction.Action createAction(ShellStore store) { + return new LaunchShortcutAction.Action(DataStorage.get().getStore(store)); + } + + @Override + public Class getApplicableClass() { + return ShellStore.class; + } + + @Override + public boolean isApplicable(ShellStore o) throws Exception { + return !ShellStore.isLocal(o); + } + + @Override + public ObservableValue getName(ShellStore store) { + return I18n.observable("installConnector"); + } + + @Override + public String getIcon(ShellStore store) { + return "mdi2c-code-greater-than"; + } + + }; + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/action/LaunchAction.java b/ext/proc/src/main/java/io/xpipe/ext/proc/action/LaunchAction.java new file mode 100644 index 00000000..61206e08 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/action/LaunchAction.java @@ -0,0 +1,89 @@ +package io.xpipe.ext.proc.action; + +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.TerminalProvider; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.extension.I18n; +import io.xpipe.extension.util.ActionProvider; +import javafx.beans.value.ObservableValue; +import lombok.Value; + +import java.util.List; +import java.util.UUID; + +public class LaunchAction implements ActionProvider { + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry entry; + + + @Override + public boolean requiresPlatform() { + return false; + } + @Override + public void execute() throws Exception { + var storeName = entry.getName(); + if (entry.getStore() instanceof ShellStore s) { + String command = s.create().prepareTerminalOpen(); + TerminalProvider.open(storeName, command); + } + } + } + + @Override + public LauncherCallSite getLauncherCallSite() { + return new LauncherCallSite() { + @Override + public String getId() { + return "launch"; + } + + @Override + public ActionProvider.Action createAction(List args) { + var entry = DataStorage.get().getStoreEntryByUuid(UUID.fromString(args.get(1))).orElseThrow(); + return new Action(entry); + } + }; + } + + @Override + public DataStoreCallSite getDataStoreCallSite() { + return new DataStoreCallSite() { + + @Override + public boolean isApplicable(DataStore o) throws Exception { + return o instanceof ShellStore; + } + + @Override + public ObservableValue getName(DataStore store) { + return I18n.observable("openShell"); + } + + @Override + public String getIcon(DataStore store) { + return "mdi2c-code-greater-than"; + } + + @Override + public ActionProvider.Action createAction(DataStore store) { + return new Action(DataStorage.get().getEntryByStore(store).orElseThrow()); + } + + @Override + public Class getApplicableClass() { + return DataStore.class; + } + + @Override + public boolean isMajor() { + return true; + } + }; + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/action/LaunchShortcutAction.java b/ext/proc/src/main/java/io/xpipe/ext/proc/action/LaunchShortcutAction.java new file mode 100644 index 00000000..48b12941 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/action/LaunchShortcutAction.java @@ -0,0 +1,67 @@ +package io.xpipe.ext.proc.action; + +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.store.ShellStore; +import io.xpipe.extension.I18n; +import io.xpipe.extension.util.ActionProvider; +import io.xpipe.extension.util.DesktopShortcuts; +import io.xpipe.extension.util.XPipeDistributionType; +import javafx.beans.value.ObservableValue; +import lombok.Value; + +public class LaunchShortcutAction implements ActionProvider { + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry entry; + + @Override + public boolean requiresPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + DesktopShortcuts.create("xpipe://launch/" + entry.getUuid().toString(), entry.getName()); + } + } + + @Override + public boolean isActive() throws Exception { + return XPipeDistributionType.get().supportsURLs(); + } + + + @Override + public DataStoreCallSite getDataStoreCallSite() { + return new DataStoreCallSite() { + + @Override + public Action createAction(ShellStore store) { + return new Action(DataStorage.get().getStore(store)); + } + + @Override + public Class getApplicableClass() { + return ShellStore.class; + } + + @Override + public ObservableValue getName(ShellStore store) { + return I18n.observable("createShortcut"); + } + + @Override + public String getIcon(ShellStore store) { + return "mdi2c-code-greater-than"; + } + + @Override + public boolean isMajor() { + return false; + } + }; + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/augment/CmdCommandAugmentation.java b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/CmdCommandAugmentation.java new file mode 100644 index 00000000..0f19d5d6 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/CmdCommandAugmentation.java @@ -0,0 +1,25 @@ +package io.xpipe.ext.proc.augment; + +import java.util.List; + +public class CmdCommandAugmentation extends CommandAugmentation { + @Override + public boolean matches(String executable) { + return executable.equals("cmd"); + } + + @Override + protected void prepareBaseCommand(List baseCommand) { + remove(baseCommand, "/C"); + } + + @Override + protected void modifyTerminalCommand(List baseCommand, boolean hasSubCommand) { + if (hasSubCommand) { + baseCommand.add("/C"); + } + } + + @Override + protected void modifyNonTerminalCommand(List baseCommand) {} +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/augment/CommandAugmentation.java b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/CommandAugmentation.java new file mode 100644 index 00000000..e1d901c9 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/CommandAugmentation.java @@ -0,0 +1,70 @@ +package io.xpipe.ext.proc.augment; + +import io.xpipe.core.process.ShellProcessControl; +import org.apache.commons.exec.CommandLine; + +import java.util.*; +import java.util.stream.Collectors; + +public abstract class CommandAugmentation { + + private static final Set ALL = new HashSet<>(); + + public static CommandAugmentation get(String cmd) { + var parsed = CommandLine.parse(cmd); + var executable = parsed.getExecutable().toLowerCase(Locale.ROOT).replaceAll("\\.exe$", ""); + if (ALL.isEmpty()) { + ALL.addAll( + ServiceLoader.load(CommandAugmentation.class.getModule().getLayer(), CommandAugmentation.class) + .stream() + .map(commandAugmentationProvider -> commandAugmentationProvider.get()) + .collect(Collectors.toSet())); + } + + return ALL.stream() + .filter(commandAugmentation -> commandAugmentation.matches(executable)) + .findFirst() + .orElse(new NoCommandAugmentation()); + } + + private static List split(String cmd) { + var parsed = CommandLine.parse(cmd); + var splitCommand = new ArrayList<>(Arrays.asList(parsed.getArguments())); + splitCommand.add(0, parsed.getExecutable().replaceAll("\\.exe$", "")); + return splitCommand; + } + + public abstract boolean matches(String executable); + + protected void addIfNeeded(List baseCommand, String arg) { + if (!baseCommand.contains(arg)) { + baseCommand.add(1, arg); + } + } + + protected void remove(List baseCommand, String... args) { + for (var arg : args) { + baseCommand.removeIf(s -> s.toLowerCase(Locale.ROOT).equals(arg)); + } + } + + public String prepareTerminalCommand(ShellProcessControl proc, String cmd, String subCommand) { + var split = split(cmd); + prepareBaseCommand(split); + modifyTerminalCommand(split, subCommand != null); + return proc.getShellType().flatten(split) + (subCommand != null ? " " + subCommand : ""); + } + + public String prepareNonTerminalCommand(ShellProcessControl proc, String cmd) { + var split = split(cmd); + prepareBaseCommand(split); + modifyNonTerminalCommand(split); + return proc.getShellType().flatten(split); + } + + protected abstract void prepareBaseCommand(List baseCommand); + + protected abstract void modifyTerminalCommand(List baseCommand, boolean hasSubCommand); + + protected abstract void modifyNonTerminalCommand(List baseCommand); +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/augment/NoCommandAugmentation.java b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/NoCommandAugmentation.java new file mode 100644 index 00000000..55611c77 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/NoCommandAugmentation.java @@ -0,0 +1,20 @@ +package io.xpipe.ext.proc.augment; + +import java.util.List; + +public class NoCommandAugmentation extends CommandAugmentation { + + @Override + public boolean matches(String executable) { + return true; + } + + @Override + protected void prepareBaseCommand(List baseCommand) {} + + @Override + protected void modifyTerminalCommand(List baseCommand, boolean hasSubCommand) {} + + @Override + protected void modifyNonTerminalCommand(List baseCommand) {} +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/augment/PosixShellCommandAugmentation.java b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/PosixShellCommandAugmentation.java new file mode 100644 index 00000000..2eaca8fa --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/PosixShellCommandAugmentation.java @@ -0,0 +1,29 @@ +package io.xpipe.ext.proc.augment; + +import java.util.List; + +public class PosixShellCommandAugmentation extends CommandAugmentation { + @Override + public boolean matches(String executable) { + return executable.equals("sh") || executable.equals("bash") || executable.equals("zsh"); + } + + @Override + protected void prepareBaseCommand(List baseCommand) { + remove(baseCommand, "-l", "--login"); + remove(baseCommand, "-i"); + remove(baseCommand, "-c"); + remove(baseCommand, "-s"); + } + + @Override + protected void modifyTerminalCommand(List baseCommand, boolean hasSubCommand) { + addIfNeeded(baseCommand, "-i"); + if (hasSubCommand) { + baseCommand.add("-c"); + } + } + + @Override + protected void modifyNonTerminalCommand(List baseCommand) {} +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/augment/PowershellCommandAugmentation.java b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/PowershellCommandAugmentation.java new file mode 100644 index 00000000..5910c066 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/PowershellCommandAugmentation.java @@ -0,0 +1,28 @@ +package io.xpipe.ext.proc.augment; + +import java.util.List; + +public class PowershellCommandAugmentation extends CommandAugmentation { + @Override + public boolean matches(String executable) { + return executable.equals("powershell") || executable.equals("pwsh"); + } + + @Override + protected void prepareBaseCommand(List baseCommand) { + remove(baseCommand, "-Command"); + remove(baseCommand, "-NonInteractive"); + } + + @Override + protected void modifyTerminalCommand(List baseCommand, boolean hasSubCommand) { + if (hasSubCommand) { + baseCommand.add("-Command"); + } + } + + @Override + protected void modifyNonTerminalCommand(List baseCommand) { + addIfNeeded(baseCommand, "-NonInteractive"); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/augment/SshCommandAugmentation.java b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/SshCommandAugmentation.java new file mode 100644 index 00000000..f2fdb1ea --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/augment/SshCommandAugmentation.java @@ -0,0 +1,32 @@ +package io.xpipe.ext.proc.augment; + +import java.util.List; + +public class SshCommandAugmentation extends CommandAugmentation { + + @Override + public boolean matches(String executable) { + return executable.equals("ssh"); + } + + @Override + protected void prepareBaseCommand(List baseCommand) { + baseCommand.removeIf(s -> s.equals("-T")); + baseCommand.removeIf(s -> s.equals("-t")); + baseCommand.removeIf(s -> s.equals("-tt")); + baseCommand.removeIf(s -> s.equals("-oStrictHostKeyChecking=yes")); + + addIfNeeded(baseCommand, "-oStrictHostKeyChecking=no"); + // addIfNeeded(baseCommand,"-oPasswordAuthentication=no"); + } + + @Override + protected void modifyTerminalCommand(List baseCommand, boolean hasSubCommand) { + addIfNeeded(baseCommand, "-t"); + } + + @Override + protected void modifyNonTerminalCommand(List baseCommand) { + addIfNeeded(baseCommand, "-T"); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/CommandStore.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/CommandStore.java new file mode 100644 index 00000000..2ca649cb --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/CommandStore.java @@ -0,0 +1,89 @@ +package io.xpipe.ext.proc.store; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.core.process.CommandProcessControl; +import io.xpipe.core.process.ShellType; +import io.xpipe.core.store.CommandExecutionStore; +import io.xpipe.core.store.DataFlow; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.store.StreamDataStore; +import io.xpipe.core.util.JacksonizedValue; +import io.xpipe.ext.proc.augment.CommandAugmentation; +import io.xpipe.extension.util.Validators; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.io.InputStream; +import java.io.OutputStream; + +@JsonTypeName("cmd") +@SuperBuilder +@Jacksonized +@Getter +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +public class CommandStore extends JacksonizedValue implements StreamDataStore, CommandExecutionStore { + + String cmd; + + ShellStore host; + + ShellType shell; + + DataFlow flow; + + boolean requiresElevation; + + public DataFlow getFlow() { + return flow; + } + + @Override + public void validate() throws Exception { + host.validate(); + } + + @Override + public void checkComplete() throws Exception { + Validators.nonNull(cmd, "Command"); + Validators.nonNull(host, "Host"); + host.checkComplete(); + Validators.nonNull(flow, "Flow"); + } + + @Override + public InputStream openInput() throws Exception { + if (!flow.hasInput()) { + throw new UnsupportedOperationException(); + } + + var cmd = create().start(); + return cmd.getStdout(); + } + + @Override + public OutputStream openOutput() throws Exception { + if (!flow.hasOutput()) { + throw new UnsupportedOperationException(); + } + + var cmd = create().start(); + return cmd.getStdin(); + } + + @Override + public CommandProcessControl create() throws Exception { + var augmentation = CommandAugmentation.get(getCmd()); + var base = shell != null ? host.create().subShell(shell) : host.create(); + var command = base.command( + proc -> augmentation.prepareNonTerminalCommand(proc, getCmd()), + proc -> augmentation.prepareTerminalCommand(proc, getCmd(), null)) + .complex(); + if (requiresElevation) { + command = command.elevated(); + } + return command; + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/CommandStoreProvider.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/CommandStoreProvider.java new file mode 100644 index 00000000..4935f73c --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/CommandStoreProvider.java @@ -0,0 +1,159 @@ +package io.xpipe.ext.proc.store; + +import io.xpipe.app.comp.base.IntegratedTextAreaComp; +import io.xpipe.core.dialog.Dialog; +import io.xpipe.core.dialog.QueryConverter; +import io.xpipe.core.impl.LocalStore; +import io.xpipe.core.process.ShellType; +import io.xpipe.core.process.ShellTypes; +import io.xpipe.core.store.DataFlow; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.extension.DataStoreProvider; +import io.xpipe.extension.GuiDialog; +import io.xpipe.extension.I18n; +import io.xpipe.extension.event.ErrorEvent; +import io.xpipe.extension.fxcomps.impl.DataStoreFlowChoiceComp; +import io.xpipe.extension.fxcomps.impl.ShellStoreChoiceComp; +import io.xpipe.extension.util.DataStoreFormatter; +import io.xpipe.extension.util.DialogHelper; +import io.xpipe.extension.util.DynamicOptionsBuilder; +import io.xpipe.extension.util.SimpleValidator; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.Arrays; +import java.util.List; + +public class CommandStoreProvider implements DataStoreProvider { + + @Override + public DataStore getParent(DataStore store) { + CommandStore s = store.asNeeded(); + return s.getHost(); + } + + @Override + public boolean isShareable() { + return true; + } + + @Override + public String getId() { + return "cmd"; + } + + @Override + public GuiDialog guiDialog(Property store) { + var val = new SimpleValidator(); + CommandStore st = (CommandStore) store.getValue(); + + Property machineProperty = + new SimpleObjectProperty<>(st.getHost() != null ? st.getHost() : new LocalStore()); + ShellType type; + try { + type = machineProperty.getValue().determineType(); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).handle(); + type = null; + } + Property shellTypeProperty = + new SimpleObjectProperty<>(st.getShell() != null ? st.getShell() : type); + Property commandProp = new SimpleObjectProperty<>(st.getCmd()); + Property flowProperty = new SimpleObjectProperty<>(st.getFlow()); + var requiresElevationProperty = new SimpleBooleanProperty(st.isRequiresElevation()); + + var q = new DynamicOptionsBuilder(I18n.observable("configuration")) + .addComp(I18n.observable("proc.host"), ShellStoreChoiceComp.host(machineProperty), machineProperty) + .nonNull(val) + .addComp( + I18n.observable("proc.shellType"), + new ShellTypeChoiceComp(shellTypeProperty), + shellTypeProperty) + .addComp( + I18n.observable("proc.command"), + new IntegratedTextAreaComp(commandProp, false, "command", "txt"), + commandProp) + .nonNull(val) + .addComp("proc.usage", new DataStoreFlowChoiceComp(flowProperty, DataFlow.values()), flowProperty) + .addToggle("requiresElevation", requiresElevationProperty) + .bind( + () -> { + return CommandStore.builder() + .cmd(commandProp.getValue()) + .host(machineProperty.getValue()) + .shell(shellTypeProperty.getValue()) + .flow(flowProperty.getValue()) + .requiresElevation(requiresElevationProperty.get()) + .build(); + }, + store) + .buildComp(); + return new GuiDialog(q, val); + } + + @Override + public String toSummaryString(DataStore store, int length) { + CommandStore s = store.asNeeded(); + return DataStoreFormatter.formatSubHost( + l -> DataStoreFormatter.cut(s.getCmd().lines().findFirst().orElse("?"), l), s.getHost(), length); + } + + @Override + public String queryInformationString(DataStore store, int length) throws Exception { + CommandStore s = store.asNeeded(); + var shellName = s.getShell() != null ? s.getShell().getDisplayName() : "Default Shell"; + return String.format("%s Command", shellName); + } + + @Override + public Category getCategory() { + return Category.STREAM; + } + + @Override + public DataStore defaultStore() { + return CommandStore.builder() + .host(new LocalStore()) + .flow(DataFlow.INPUT) + .build(); + } + + @Override + public List getPossibleNames() { + return List.of("cmd", "command", "shell", "run", "execute"); + } + + @Override + public List> getStoreClasses() { + return List.of(CommandStore.class); + } + + @Override + public Dialog dialogForStore(DataStore store) { + CommandStore commandStore = store.asNeeded(); + var cmdQ = Dialog.query("Command", true, true, false, commandStore.getCmd(), QueryConverter.STRING); + var machineQ = DialogHelper.shellQuery("Command Host", commandStore.getHost()); + var shellQuery = Dialog.lazy(() -> { + var available = Arrays.stream(ShellTypes.getAllShellTypes()).toList(); + return Dialog.choice( + "Shell Type", + t -> t.getDisplayName(), + true, + false, + available.get(0), + available.toArray(ShellType[]::new)); + }); + var flowQuery = DialogHelper.dataStoreFlowQuery(commandStore.getFlow(), DataFlow.values()); + + return Dialog.chain(cmdQ, machineQ, shellQuery, flowQuery).evaluateTo(() -> { + return CommandStore.builder() + .cmd(cmdQ.getResult()) + .host(machineQ.getResult()) + .shell(shellQuery.getResult()) + .flow(flowQuery.getResult()) + .build(); + }); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/DockerStore.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/DockerStore.java new file mode 100644 index 00000000..9015701c --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/DockerStore.java @@ -0,0 +1,69 @@ +package io.xpipe.ext.proc.store; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.process.ShellTypes; +import io.xpipe.core.store.MachineStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.util.JacksonizedValue; +import io.xpipe.extension.util.Validators; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +@JsonTypeName("docker") +@SuperBuilder +@Jacksonized +@Getter +public class DockerStore extends JacksonizedValue implements MachineStore { + + private final ShellStore host; + private final String containerName; + + public DockerStore(ShellStore host, String containerName) { + this.host = host; + this.containerName = containerName; + } + + public static boolean isSupported(ShellStore host) { + return host.create().command("docker --help").startAndCheckExit(); + } + + @Override + public void checkComplete() throws Exception { + Validators.nonNull(host, "Host"); + host.checkComplete(); + Validators.nonNull(containerName, "Name"); + } + + @Override + public void validate() throws Exception { + host.validate(); + Validators.hostFeature(host, DockerStore::isSupported, "docker"); + + try (var pc = host.create() + .command(List.of("docker", "container", "inspect", containerName)) + .elevated() + .start()) { + pc.discardOrThrow(); + } + } + + @Override + public ShellProcessControl createControl() { + return host.create() + .subShell( + shellProcessControl -> + "docker exec -i " + containerName + " " + ShellTypes.BASH.getNormalOpenCommand(), + (shellProcessControl, s) -> { + if (s != null) { + return "docker exec -it " + containerName + " " + s; + } else { + return "docker exec -it " + containerName; + } + }) + .elevated(shellProcessControl -> true); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/DockerStoreProvider.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/DockerStoreProvider.java new file mode 100644 index 00000000..eb7ed158 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/DockerStoreProvider.java @@ -0,0 +1,98 @@ +package io.xpipe.ext.proc.store; + +import io.xpipe.core.dialog.Dialog; +import io.xpipe.core.dialog.QueryConverter; +import io.xpipe.core.impl.LocalStore; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.extension.DataStoreProvider; +import io.xpipe.extension.GuiDialog; +import io.xpipe.extension.I18n; +import io.xpipe.extension.fxcomps.impl.ShellStoreChoiceComp; +import io.xpipe.extension.util.DataStoreFormatter; +import io.xpipe.extension.util.DialogHelper; +import io.xpipe.extension.util.DynamicOptionsBuilder; +import io.xpipe.extension.util.SimpleValidator; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.List; + +public class DockerStoreProvider implements DataStoreProvider { + + @Override + public DataStore getParent(DataStore store) { + DockerStore s = store.asNeeded(); + return s.getHost(); + } + + @Override + public boolean isShareable() { + return true; + } + + @Override + public GuiDialog guiDialog(Property store) { + var val = new SimpleValidator(); + DockerStore st = (DockerStore) store.getValue(); + + Property shellProp = + new SimpleObjectProperty<>(st.getHost() != null ? st.getHost() : new LocalStore()); + Property containerProp = new SimpleObjectProperty<>(st.getContainerName()); + + var q = new DynamicOptionsBuilder(I18n.observable("configuration")) + .addComp( + I18n.observable("host"), + ShellStoreChoiceComp.host(st, shellProp), + shellProp) + .nonNull(val) + .addString(I18n.observable("proc.container"), containerProp) + .nonNull(val) + .bind( + () -> { + return new DockerStore(shellProp.getValue(), containerProp.getValue()); + }, + store) + .buildComp(); + return new GuiDialog(q, val); + } + + @Override + public String queryInformationString(DataStore store, int length) throws Exception { + DockerStore s = store.asNeeded(); + return String.format("%s %s", s.queryMachineName(), I18n.get("proc.container")); + } + + @Override + public String toSummaryString(DataStore store, int length) { + DockerStore s = store.asNeeded(); + return DataStoreFormatter.formatSubHost( + l -> DataStoreFormatter.cut(s.getContainerName(), l), s.getHost(), length); + } + + @Override + public List> getStoreClasses() { + return List.of(DockerStore.class); + } + + @Override + public DataStore defaultStore() { + return new DockerStore(new LocalStore(), null); + } + + @Override + public Dialog dialogForStore(DataStore store) { + DockerStore dockerStore = store.asNeeded(); + var hostQ = DialogHelper.machineQuery(dockerStore.getHost()); + var containerQ = + Dialog.query("Container", false, true, false, dockerStore.getContainerName(), QueryConverter.STRING); + return Dialog.chain(hostQ, containerQ).evaluateTo(() -> { + return new DockerStore(hostQ.getResult(), containerQ.getResult()); + }); + } + + @Override + public List getPossibleNames() { + return List.of("docker", "docker_container"); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellCommandStore.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellCommandStore.java new file mode 100644 index 00000000..0bebefe5 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellCommandStore.java @@ -0,0 +1,51 @@ +package io.xpipe.ext.proc.store; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.process.ShellType; +import io.xpipe.core.store.MachineStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.util.JacksonizedValue; +import io.xpipe.ext.proc.augment.CommandAugmentation; +import io.xpipe.extension.util.Validators; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@SuperBuilder +@Getter +@Jacksonized +@JsonTypeName("shellCommand") +public class ShellCommandStore extends JacksonizedValue implements MachineStore { + + private final String cmd; + private final ShellStore host; + public ShellCommandStore(String cmd, ShellStore host) { + this.cmd = cmd; + this.host = host; + } + + public static ShellCommandStore shell(ShellStore host, ShellType type) { + return ShellCommandStore.builder() + .host(host) + .cmd(type.getNormalOpenCommand()) + .build(); + } + + @Override + public void checkComplete() throws Exception { + Validators.nonNull(cmd, "Command"); + Validators.nonNull(host, "Host"); + Validators.namedStoreExists(host, "Host"); + host.checkComplete(); + } + + @Override + public ShellProcessControl createControl() { + var augmentation = CommandAugmentation.get(getCmd()); + return host.create() + .subShell( + proc -> augmentation.prepareNonTerminalCommand(proc, getCmd()), + (proc, s) -> augmentation.prepareTerminalCommand(proc, getCmd(), s)); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellCommandStoreProvider.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellCommandStoreProvider.java new file mode 100644 index 00000000..224f52cc --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellCommandStoreProvider.java @@ -0,0 +1,113 @@ +package io.xpipe.ext.proc.store; + +import io.xpipe.core.dialog.Dialog; +import io.xpipe.core.dialog.QueryConverter; +import io.xpipe.core.impl.LocalStore; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.extension.DataStoreProvider; +import io.xpipe.extension.DataStoreProviders; +import io.xpipe.extension.GuiDialog; +import io.xpipe.extension.I18n; +import io.xpipe.extension.fxcomps.impl.ShellStoreChoiceComp; +import io.xpipe.extension.util.DataStoreFormatter; +import io.xpipe.extension.util.DialogHelper; +import io.xpipe.extension.util.DynamicOptionsBuilder; +import io.xpipe.extension.util.SimpleValidator; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.List; + +public class ShellCommandStoreProvider implements DataStoreProvider { + + @Override + public DataStore getParent(DataStore store) { + ShellCommandStore s = store.asNeeded(); + return s.getHost(); + } + + @Override + public boolean isShareable() { + return true; + } + + @Override + public String getId() { + return "shellCommand"; + } + + @Override + public GuiDialog guiDialog(Property store) { + var val = new SimpleValidator(); + ShellCommandStore st = (ShellCommandStore) store.getValue(); + + Property hostProperty = new SimpleObjectProperty<>(st.getHost()); + Property commandProp = new SimpleObjectProperty<>(st.getCmd()); + + var q = new DynamicOptionsBuilder(I18n.observable("configuration")) + .addComp( + I18n.observable("host"), + ShellStoreChoiceComp.host(st, hostProperty), + hostProperty) + .nonNull(val) + .addString(I18n.observable("proc.command"), commandProp) + .nonNull(val) + .bind( + () -> { + return new ShellCommandStore(commandProp.getValue(), hostProperty.getValue()); + }, + store) + .buildComp(); + return new GuiDialog(q, val); + } + + @Override + public String queryInformationString(DataStore store, int length) throws Exception { + ShellCommandStore s = store.asNeeded(); + return s.queryMachineName(); + } + + @Override + public String toSummaryString(DataStore store, int length) { + ShellCommandStore s = store.asNeeded(); + var local = ShellStore.isLocal(s.getHost()); + if (local) { + return DataStoreFormatter.cut(s.getCmd(), length); + } else { + var machineString = DataStoreProviders.byStore(s.getHost()).toSummaryString(s.getHost(), length / 2); + var fileString = DataStoreFormatter.cut(s.getCmd().toString(), length - machineString.length() - 3); + return String.format("%s @ %s", fileString, machineString); + } + } + + @Override + public Category getCategory() { + return Category.SHELL; + } + + @Override + public List> getStoreClasses() { + return List.of(ShellCommandStore.class); + } + + @Override + public DataStore defaultStore() { + return new ShellCommandStore(null, new LocalStore()); + } + + @Override + public List getPossibleNames() { + return List.of("shell_command"); + } + + @Override + public Dialog dialogForStore(DataStore store) { + ShellCommandStore s = store.asNeeded(); + var hostQ = DialogHelper.shellQuery("Command Host", s.getHost()); + var commandQ = Dialog.query("Command", true, true, false, s.getCmd(), QueryConverter.STRING); + return Dialog.chain(hostQ, commandQ).evaluateTo(() -> { + return new ShellCommandStore(commandQ.getResult(), hostQ.getResult()); + }); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellEnvironmentStore.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellEnvironmentStore.java new file mode 100644 index 00000000..a15df23c --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellEnvironmentStore.java @@ -0,0 +1,54 @@ +package io.xpipe.ext.proc.store; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.process.ShellType; +import io.xpipe.core.store.MachineStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.util.JacksonizedValue; +import io.xpipe.extension.util.Validators; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@SuperBuilder +@Getter +@Jacksonized +@JsonTypeName("shellEnvironment") +public class ShellEnvironmentStore extends JacksonizedValue implements MachineStore { + + private final String commands; + private final ShellStore host; + private final ShellType shell; + + + public ShellEnvironmentStore(String commands, ShellStore host, ShellType shell) { + this.commands = commands; + this.host = host; + this.shell = shell; + } + + @Override + public void checkComplete() throws Exception { + Validators.nonNull(commands, "Commands"); + Validators.nonNull(host, "Host"); + Validators.namedStoreExists(host, "Host"); + host.checkComplete(); + } + + @Override + public void validate() throws Exception { + try (var ignored = create().start()) { + + } + } + + @Override + public ShellProcessControl createControl() { + var pc = host.create(); + if (shell != null) { + pc = pc.subShell(shell); + } + return pc.initWith(commands.lines().toList()); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellEnvironmentStoreProvider.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellEnvironmentStoreProvider.java new file mode 100644 index 00000000..20cc689a --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellEnvironmentStoreProvider.java @@ -0,0 +1,102 @@ +package io.xpipe.ext.proc.store; + +import io.xpipe.app.comp.base.IntegratedTextAreaComp; +import io.xpipe.core.impl.LocalStore; +import io.xpipe.core.process.ShellType; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.util.Identifiers; +import io.xpipe.extension.DataStoreProvider; +import io.xpipe.extension.GuiDialog; +import io.xpipe.extension.I18n; +import io.xpipe.extension.fxcomps.impl.ShellStoreChoiceComp; +import io.xpipe.extension.util.DynamicOptionsBuilder; +import io.xpipe.extension.util.SimpleValidator; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.List; + +public class ShellEnvironmentStoreProvider implements DataStoreProvider { + + @Override + public boolean isShareable() { + return true; + } + + @Override + public String getId() { + return "shellEnvironment"; + } + + @Override + public GuiDialog guiDialog(Property store) { + var val = new SimpleValidator(); + ShellEnvironmentStore st = store.getValue().asNeeded(); + + Property hostProperty = new SimpleObjectProperty<>(st.getHost()); + Property commandProp = new SimpleObjectProperty<>(st.getCommands()); + Property shellTypeProperty = new SimpleObjectProperty<>(st.getShell()); + + var q = new DynamicOptionsBuilder(I18n.observable("configuration")) + .addComp(I18n.observable("host"), ShellStoreChoiceComp.host(st, hostProperty), hostProperty) + .nonNull(val) + .addComp( + I18n.observable("proc.shellType"), + new ShellTypeChoiceComp(shellTypeProperty), + shellTypeProperty) + .addComp( + I18n.observable("proc.commands"), + new IntegratedTextAreaComp(commandProp, false, "commands", "txt"), + commandProp) + .nonNull(val) + .bind( + () -> { + return new ShellEnvironmentStore( + commandProp.getValue(), hostProperty.getValue(), shellTypeProperty.getValue()); + }, + store) + .buildComp(); + return new GuiDialog(q, val); + } + + @Override + public String queryInformationString(DataStore store, int length) throws Exception { + ShellEnvironmentStore s = store.asNeeded(); + var name = s.getShell() != null ? s.getShell().getDisplayName() : "Default"; + return I18n.get("shellEnvironment.informationFormat", name); + } + + @Override + public String toSummaryString(DataStore store, int length) { + ShellEnvironmentStore s = store.asNeeded(); + var commandSummary = "<" + s.getCommands().lines().count() + " commands>"; + return commandSummary; + } + + @Override + public DataStore getParent(DataStore store) { + ShellEnvironmentStore s = store.asNeeded(); + return s.getHost(); + } + + @Override + public Category getCategory() { + return Category.SHELL; + } + + @Override + public List> getStoreClasses() { + return List.of(ShellEnvironmentStore.class); + } + + @Override + public DataStore defaultStore() { + return new ShellEnvironmentStore(null, new LocalStore(), null); + } + + @Override + public List getPossibleNames() { + return Identifiers.get("shell", "environment"); + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellTypeChoiceComp.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellTypeChoiceComp.java new file mode 100644 index 00000000..e4e8838b --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/ShellTypeChoiceComp.java @@ -0,0 +1,66 @@ +package io.xpipe.ext.proc.store; + +import io.xpipe.core.process.ShellType; +import io.xpipe.core.process.ShellTypes; +import io.xpipe.extension.I18n; +import io.xpipe.extension.fxcomps.SimpleComp; +import io.xpipe.extension.util.CustomComboBoxBuilder; +import io.xpipe.extension.util.XPipeDaemon; +import javafx.beans.property.Property; +import javafx.scene.Node; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; +import lombok.AllArgsConstructor; + +import java.util.Arrays; +import java.util.Map; + +@AllArgsConstructor +public class ShellTypeChoiceComp extends SimpleComp { + + public static final Map ICONS = Map.of( + ShellTypes.CMD, "cmd.png", + ShellTypes.POWERSHELL, "powershell.png", + ShellTypes.ZSH, "cmd.png", + ShellTypes.SH, "cmd.png", + ShellTypes.BASH, "cmd.png"); + + private final Property selected; + + private Region createGraphic(ShellType s) { + if (s == null) { + return createEmptyGraphic(); + } + + var img = XPipeDaemon.getInstance().image("base:" + ICONS.get(s)); + var imgView = new ImageView(img); + imgView.setFitWidth(16); + imgView.setFitHeight(16); + + var name = s.getDisplayName(); + + return new Label(name, imgView); + } + + private Region createEmptyGraphic() { + var img = XPipeDaemon.getInstance().image("proc:defaultShell_icon.png"); + var imgView = new ImageView(img); + imgView.setFitWidth(16); + imgView.setFitHeight(16); + + return new Label(I18n.get("default"), imgView); + } + + @Override + protected Region createSimple() { + var comboBox = new CustomComboBoxBuilder<>(selected, this::createGraphic, null, e -> true); + comboBox.add(null); + Arrays.stream(ShellTypes.getAllShellTypes()).forEach(shellType -> comboBox.add(shellType)); + ComboBox cb = comboBox.build(); + cb.getStyleClass().add("choice-comp"); + cb.setMaxWidth(2000); + return cb; + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/SshStore.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/SshStore.java new file mode 100644 index 00000000..267ae348 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/SshStore.java @@ -0,0 +1,64 @@ +package io.xpipe.ext.proc.store; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.core.process.ProcessControlProvider; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.store.MachineStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.util.JacksonizedValue; +import io.xpipe.core.util.SecretValue; +import io.xpipe.extension.util.Validators; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@SuperBuilder +@Jacksonized +@Getter +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +@JsonTypeName("ssh") +public class SshStore extends JacksonizedValue implements MachineStore { + + ShellStore proxy; + String host; + Integer port; + String user; + SecretValue password; + SshKey key; + public SshStore(ShellStore proxy, String host, Integer port, String user, SecretValue password, SshKey key) { + this.proxy = proxy; + this.host = host; + this.port = port; + this.user = user; + this.password = password; + this.key = key; + } + + @Override + public void checkComplete() throws Exception { + Validators.nonNull(proxy, "Proxy"); + Validators.nonNull(host, "Host"); + Validators.nonNull(port, "Port"); + Validators.nonNull(user, "User"); + + proxy.checkComplete(); + } + + @Override + public ShellProcessControl createControl() { + return ProcessControlProvider.createSsh(this); + } + + @SuperBuilder + @Jacksonized + @Getter + public static class SshKey { + @NonNull + String file; + + SecretValue password; + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/SshStoreProvider.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/SshStoreProvider.java new file mode 100644 index 00000000..2834ea76 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/SshStoreProvider.java @@ -0,0 +1,162 @@ +package io.xpipe.ext.proc.store; + +import io.xpipe.core.dialog.Dialog; +import io.xpipe.core.impl.FileStore; +import io.xpipe.core.impl.LocalStore; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.util.SecretValue; +import io.xpipe.extension.DataStoreProvider; +import io.xpipe.extension.GuiDialog; +import io.xpipe.extension.I18n; +import io.xpipe.extension.fxcomps.Comp; +import io.xpipe.extension.fxcomps.impl.FileStoreChoiceComp; +import io.xpipe.extension.fxcomps.impl.SecretFieldComp; +import io.xpipe.extension.fxcomps.impl.ShellStoreChoiceComp; +import io.xpipe.extension.util.DataStoreFormatter; +import io.xpipe.extension.util.DialogHelper; +import io.xpipe.extension.util.DynamicOptionsBuilder; +import io.xpipe.extension.util.SimpleValidator; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; + +import java.util.List; + +public class SshStoreProvider implements DataStoreProvider { + + @Override + public DataStore getParent(DataStore store) { + SshStore s = store.asNeeded(); + return !ShellStore.isLocal(s.getProxy()) ? s.getProxy() : null; + } + + @Override + public boolean isShareable() { + return true; + } + + @Override + public GuiDialog guiDialog(Property store) { + var val = new SimpleValidator(); + SshStore st = (SshStore) store.getValue(); + + var shellProp = new SimpleObjectProperty<>(st.getProxy()); + var host = new SimpleObjectProperty<>(st.getHost() != null ? st.getHost() : null); + var port = new SimpleObjectProperty<>(st.getPort()); + var user = new SimpleStringProperty(st.getUser()); + var pass = new SimpleObjectProperty<>(st.getPassword()); + + var key = new SimpleObjectProperty<>(st.getKey()); + + var q = new DynamicOptionsBuilder(I18n.observable("configuration")) + .addComp( + I18n.observable("proxy"), + ShellStoreChoiceComp.host(st, shellProp), + shellProp) + .nonNull(val) + .addString(I18n.observable("host"), host) + .nonNull(val) + .addInteger(I18n.observable("port"), port) + .nonNull(val) + .addString(I18n.observable("user"), user) + .nonNull(val) + .addSecret(I18n.observable("password"), pass) + .addComp(keyFileConfig(key), key) + .bind( + () -> { + return new SshStore( + shellProp.get(), host.get(), port.get(), user.get(), pass.get(), key.get()); + }, + store) + .buildComp(); + return new GuiDialog(q, val); + } + + private Comp keyFileConfig(ObjectProperty key) { + var keyFileProperty = new SimpleObjectProperty( + key.get() != null + ? FileStore.builder() + .fileSystem(new LocalStore()) + .file(key.get().getFile().toString()) + .build() + : null); + var keyPasswordProperty = new SimpleObjectProperty( + key.get() != null ? key.get().getPassword() : null); + + return new DynamicOptionsBuilder(false) + .addTitle("key") + .addComp( + "keyFile", new FileStoreChoiceComp(List.of(new LocalStore()), keyFileProperty), keyFileProperty) + .addComp("keyPassword", new SecretFieldComp(keyPasswordProperty), keyPasswordProperty) + .bind( + () -> { + return keyFileProperty.get() != null + ? SshStore.SshKey.builder() + .file(keyFileProperty + .get() + .getFile()) + .password(keyPasswordProperty.get()) + .build() + : null; + }, + key) + .buildComp(); + } + + @Override + public String queryInformationString(DataStore store, int length) throws Exception { + SshStore s = store.asNeeded(); + return s.queryMachineName(); + } + + @Override + public String toSummaryString(DataStore store, int length) { + SshStore s = store.asNeeded(); + var portSuffix = s.getPort() == 22 ? "" : ":" + s.getPort(); + return DataStoreFormatter.formatViaProxy( + l -> { + var hostNameLength = + Math.max(l - portSuffix.length() - s.getUser().length() - 1, 0); + return s.getUser() + "@" + DataStoreFormatter.formatHostName(s.getHost(), hostNameLength) + + portSuffix; + }, + s.getProxy(), + length); + } + + @Override + public List> getStoreClasses() { + return List.of(SshStore.class); + } + + @Override + public DataStore defaultStore() { + return new SshStore(ShellStore.local(), null, 22, null, null, null); + } + + @Override + public List getPossibleNames() { + return List.of("ssh"); + } + + @Override + public Dialog dialogForStore(DataStore store) { + SshStore sshStore = store.asNeeded(); + var address = new DialogHelper.Address(sshStore.getHost(), sshStore.getPort()); + var addressQuery = DialogHelper.addressQuery(address); + var usernameQuery = DialogHelper.userQuery(sshStore.getUser()); + var passwordQuery = DialogHelper.passwordQuery(sshStore.getPassword()); + return Dialog.chain(addressQuery, usernameQuery, passwordQuery).evaluateTo(() -> { + DialogHelper.Address newAddress = addressQuery.getResult(); + return new SshStore( + ShellStore.local(), + newAddress.getHostname(), + newAddress.getPort(), + usernameQuery.getResult(), + passwordQuery.getResult(), + null); + }); + } +} \ No newline at end of file diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/WslStore.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/WslStore.java new file mode 100644 index 00000000..07ad74da --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/WslStore.java @@ -0,0 +1,111 @@ +package io.xpipe.ext.proc.store; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.store.MachineStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.util.JacksonizedValue; +import io.xpipe.extension.event.ErrorEvent; +import io.xpipe.extension.util.Validators; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@SuperBuilder +@Jacksonized +@Getter +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@JsonTypeName("wsl") +public class WslStore extends JacksonizedValue implements MachineStore { + + ShellStore host; + String distribution; + String user; + + public WslStore(ShellStore host, String distribution, String user) { + this.host = host; + this.distribution = distribution; + this.user = user; + } + + public static boolean isSupported(ShellStore host) { + return ShellStore.local() + .create() + .command("wsl --list") + .customCharset(StandardCharsets.UTF_16LE) + .startAndCheckExit(); + } + + public static Optional getDefaultDistribution(ShellStore host) { + String s = null; + try (var pc = host.create() + .command("wsl --list") + .customCharset(StandardCharsets.UTF_16LE) + .start()) { + s = pc.readOnlyStdout(); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).handle(); + return Optional.empty(); + } + + var def = s.lines() + .skip(1) + .filter(line -> { + return line.trim().endsWith("(Default)"); + }) + .findFirst(); + return def.map(line -> line.replace(" (Default)", "")); + } + + @Override + public void checkComplete() throws Exception { + Validators.nonNull(host, "Host"); + host.checkComplete(); + Validators.nonNull(distribution, "Distribution"); + Validators.nonNull(user, "User"); + } + + @Override + public void validate() throws Exception { + Validators.hostFeature(host, WslStore::isSupported, "wsl"); + } + + @Override + public ShellProcessControl createControl() { + var l = createCommand(); + return host.create() + .subShell( + shellProcessControl -> + shellProcessControl.getShellType().flatten(l), + (shellProcessControl, s) -> { + var flattened = shellProcessControl.getShellType().flatten(l); + if (s != null) { + flattened += " " + s; + } + return flattened; + }); + } + + private List createCommand() { + var l = new ArrayList(List.of("wsl")); + + if (user != null) { + l.add("-u"); + l.add(user); + } + + if (distribution != null) { + l.add("--distribution"); + l.add(distribution); + } + + return l; + } +} diff --git a/ext/proc/src/main/java/io/xpipe/ext/proc/store/WslStoreProvider.java b/ext/proc/src/main/java/io/xpipe/ext/proc/store/WslStoreProvider.java new file mode 100644 index 00000000..36bdcfb7 --- /dev/null +++ b/ext/proc/src/main/java/io/xpipe/ext/proc/store/WslStoreProvider.java @@ -0,0 +1,118 @@ +package io.xpipe.ext.proc.store; + +import io.xpipe.core.dialog.Dialog; +import io.xpipe.core.dialog.QueryConverter; +import io.xpipe.core.impl.LocalStore; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.extension.DataStoreProvider; +import io.xpipe.extension.GuiDialog; +import io.xpipe.extension.I18n; +import io.xpipe.extension.fxcomps.impl.ShellStoreChoiceComp; +import io.xpipe.extension.util.DataStoreFormatter; +import io.xpipe.extension.util.DialogHelper; +import io.xpipe.extension.util.DynamicOptionsBuilder; +import io.xpipe.extension.util.SimpleValidator; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.List; + +public class WslStoreProvider implements DataStoreProvider { + + @Override + public DataStore getParent(DataStore store) { + WslStore s = store.asNeeded(); + return s.getHost(); + } + + @Override + public GuiDialog guiDialog(Property store) { + var val = new SimpleValidator(); + WslStore st = (WslStore) store.getValue(); + Property shellProp = new SimpleObjectProperty<>(st.getHost()); + Property distProp = new SimpleObjectProperty<>(st.getDistribution()); + shellProp.addListener((observable, oldValue, newValue) -> { + distProp.setValue(WslStore.getDefaultDistribution(newValue).orElse(null)); + }); + + Property userProp = new SimpleObjectProperty<>(st.getUser()); + var q = new DynamicOptionsBuilder(I18n.observable("configuration")) + .addComp( + I18n.observable("host"), + ShellStoreChoiceComp.host(st, shellProp), + shellProp) + .nonNull(val) + .addString(I18n.observable("proc.distribution"), distProp) + .nonNull(val) + .addString(I18n.observable("proc.username"), userProp) + .nonNull(val) + .bind( + () -> { + return new WslStore(shellProp.getValue(), distProp.getValue(), userProp.getValue()); + }, + store) + .buildComp(); + return new GuiDialog(q, val); + } + + @Override + public boolean isShareable() { + return true; + } + + @Override + public String queryInformationString(DataStore store, int length) throws Exception { + WslStore s = store.asNeeded(); + return String.format("%s %s", "WSL", s.queryMachineName()); + } + + @Override + public String toSummaryString(DataStore store, int length) { + WslStore s = store.asNeeded(); + return DataStoreFormatter.formatSubHost( + value -> DataStoreFormatter.cut(s.getUser() + "@" + s.getDistribution(), value), s.getHost(), length); + } + + @Override + public List> getStoreClasses() { + return List.of(WslStore.class); + } + + @Override + public DataStore defaultStore() { + return new WslStore( + new LocalStore(), + WslStore.getDefaultDistribution(new LocalStore()).orElse(null), + "root"); + } + + @Override + public List getPossibleNames() { + return List.of("wsl"); + } + + @Override + public Dialog dialogForStore(DataStore store) { + WslStore wslStore = store.asNeeded(); + var hostQ = DialogHelper.shellQuery("WSL Host", wslStore.getHost()); + var distQ = Dialog.lazy(() -> Dialog.query( + "Distribution", + false, + true, + false, + wslStore.getDistribution() != null + ? wslStore.getDistribution() + : WslStore.getDefaultDistribution(hostQ.getResult()).orElse(null), + QueryConverter.STRING)); + var userQ = Dialog.query("Username", false, true, false, wslStore.getUser(), QueryConverter.STRING); + return Dialog.chain(hostQ, distQ, userQ).evaluateTo(() -> { + return new WslStore(hostQ.getResult(), distQ.getResult(), userQ.getResult()); + }); + } + + @Override + public String getDisplayIconFileName() { + return "proc:wsl_icon.svg"; + } +} diff --git a/ext/proc/src/main/java/module-info.java b/ext/proc/src/main/java/module-info.java new file mode 100644 index 00000000..980e3840 --- /dev/null +++ b/ext/proc/src/main/java/module-info.java @@ -0,0 +1,50 @@ +import io.xpipe.core.process.ProcessControlProvider; +import io.xpipe.ext.proc.*; +import io.xpipe.ext.proc.action.InstallConnectorAction; +import io.xpipe.ext.proc.action.LaunchAction; +import io.xpipe.ext.proc.action.LaunchShortcutAction; +import io.xpipe.ext.proc.augment.*; +import io.xpipe.ext.proc.store.*; +import io.xpipe.extension.DataStoreProvider; +import io.xpipe.extension.prefs.PrefsProvider; +import io.xpipe.extension.util.ActionProvider; + +open module io.xpipe.ext.proc { + uses io.xpipe.ext.proc.augment.CommandAugmentation; + + exports io.xpipe.ext.proc; + exports io.xpipe.ext.proc.store; + exports io.xpipe.ext.proc.augment; + + requires static lombok; + requires static javafx.base; + requires static javafx.controls; + requires io.xpipe.core; + requires com.fasterxml.jackson.databind; + requires static com.jcraft.jsch; + requires static io.xpipe.extension; + requires static io.xpipe.app; + requires static commons.exec; + requires static com.dlsc.preferencesfx; + + provides PrefsProvider with + ProcPrefs; + provides CommandAugmentation with + SshCommandAugmentation, + CmdCommandAugmentation, + PosixShellCommandAugmentation, + PowershellCommandAugmentation; + provides ProcessControlProvider with + ProcProvider; + provides ActionProvider with + InstallConnectorAction, + LaunchShortcutAction, + LaunchAction; + provides DataStoreProvider with + SshStoreProvider, + ShellEnvironmentStoreProvider, + CommandStoreProvider, + DockerStoreProvider, + ShellCommandStoreProvider, + WslStoreProvider; +} diff --git a/ext/proc/src/main/resources/META-INF/services/io.xpipe.core.process.ProcessControlProvider b/ext/proc/src/main/resources/META-INF/services/io.xpipe.core.process.ProcessControlProvider new file mode 100644 index 00000000..5f1fcbb3 --- /dev/null +++ b/ext/proc/src/main/resources/META-INF/services/io.xpipe.core.process.ProcessControlProvider @@ -0,0 +1 @@ +io.xpipe.ext.proc.ProcProvider \ No newline at end of file diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/extension.properties b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/extension.properties new file mode 100644 index 00000000..d5fd4ca4 --- /dev/null +++ b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/extension.properties @@ -0,0 +1 @@ +name=proc \ No newline at end of file diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/cmd_icon.png b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/cmd_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5e180c546e0d247d60c3e21fa202588188b76d91 GIT binary patch literal 3004 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 WIsw8x$mgs)7J&T2K4P6ao&PttEzRrz literal 0 HcmV?d00001 diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/docker_icon.png b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/docker_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..95ad9e19be159914b61b46078ff7862191ad672d GIT binary patch literal 6820 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 literal 0 HcmV?d00001 diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/shellCommand_icon.png b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/shellCommand_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..54ec9b7af69905beb82cb83e7adab95d5518aae9 GIT binary patch literal 6376 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 Gjr8I5R|p#k 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 IrsWv@9~G{rNB{r; literal 0 HcmV?d00001 diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/ssh_icon.png b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/ssh_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..47ecd0d96d02c825e298c6ba50d24ceeecfff632 GIT binary patch literal 14502 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/lang/translations_de.properties b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/lang/translations_de.properties new file mode 100644 index 00000000..d86dcac5 --- /dev/null +++ b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/lang/translations_de.properties @@ -0,0 +1,3 @@ +displayName=Mein Dateiformat +description=Meine Dateiformat-Beschreibung +fileName=Mein Dateiformat Datei \ No newline at end of file diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/lang/translations_en.properties b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/lang/translations_en.properties new file mode 100644 index 00000000..87b5fe0b --- /dev/null +++ b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/lang/translations_en.properties @@ -0,0 +1,96 @@ +http.displayName=HTTP Request/Response +http.displayDescription=Handle body data of an HTTP request/response +cmd.displayName=Command +cmd.displayDescription=Handle the input and output of a command +file.displayName=File +file.displayDescription=Specify a file input +inMemory.displayName=In Memory +inMemory.displayDescription=Store binary data in memory +shellCommand.displayName=Shell Command +shellCommand.displayDescription=Open a shell with a custom command +shellEnvironment.displayName=Shell Environment +shellEnvironment.displayDescription=Run custom init commands on the shell connection +shellEnvironment.informationFormat=$TYPE$ environment +ssh.displayName=SSH Connection +ssh.displayDescription=Connect via SSH +binary.displayName=Binary +binary.displayDescription=Binary data +text.displayName=Text +text.displayDescription=Textural data start in plain text +sinkDrain.displayName=Sink Drain +sinkDrain.displayDescription=Construct a new sink drain +usage=Usage +anyBinaryFile=Any binary file +dataFile=Data file +binaryFile=Binary file +commandLine=Command Line +java=Java +streamOutput=Stream Output +localMachine=Local Machine +configuration=Configuration +selectOutput=Select Output +options=Options +requiresElevation=Run Elevated +commands=Commands +selectStore=Select Store +selectSource=Select Source +commandLineRead=Read +default=Default +commandLineWrite=Write +wslHost=WSL Host +timeout=Timeout +wsl.displayName=Windows Subsystem for Linux +wsl.displayDescription=Connect to a WSL instance running on Windows +docker.displayName=Docker Container +docker.displayDescription=Connect to a docker container +fileOutput=File Output +rawStreamOutput=Raw Stream Output +dataSourceOutput=Data Source Output +type=Type +input=Input +machine=Machine +bytes=$N$ bytes +container=Container +createShortcut=Create desktop shortcut +host=Host +port=Port +user=User +password=Password +method=Method +uri=URL +proxy=Proxy +distribution=Distribution +username=Username +terminal=Terminal +terminalProgram=Terminal program +program=Program +customTerminalCommand=Custom terminal command +cmd=cmd.exe +powershell=Powershell +windowsTerminal=Windows Terminal +gnomeTerminal=Gnome Terminal +shellType=Shell Type +command=Command +target=Target +writeMode=Write Mode +exportStream=Export Stream +browseFile=Browse File +openShell=Open Shell in Terminal +openCommand=Execute Command in Terminal +editFile=Edit File +description=Description +unconnected=Unconnected +waitingForConsumer=Waiting for Consumer +waitingForProducer=Waiting for Producer +open=Open +closed=Closed +keyFile=Key File +keyPassword=Key Password +key=Key +installConnector=Install Connector +konsole=Konsole +xfce=Xfce +macosTerminal=Terminal +iterm2=iTerm2 +warp=Warp +custom=Custom \ No newline at end of file diff --git a/ext/proc/src/test/java/module-info.java b/ext/proc/src/test/java/module-info.java new file mode 100644 index 00000000..13fb9a3b --- /dev/null +++ b/ext/proc/src/test/java/module-info.java @@ -0,0 +1,12 @@ +open module io.xpipe.ext.text.test { + requires io.xpipe.ext.proc; + requires org.junit.jupiter.api; + requires org.junit.jupiter.params; + requires io.xpipe.core; + requires io.xpipe.api; + requires io.xpipe.extension; + requires static lombok; + requires org.apache.commons.lang3; + + exports test; +} diff --git a/ext/proc/src/test/java/test/CommandStoreTest.java b/ext/proc/src/test/java/test/CommandStoreTest.java new file mode 100644 index 00000000..fce61d91 --- /dev/null +++ b/ext/proc/src/test/java/test/CommandStoreTest.java @@ -0,0 +1,64 @@ +package test; + +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.process.ShellTypes; +import io.xpipe.core.store.DataFlow; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.util.SecretValue; +import io.xpipe.ext.proc.store.CommandStore; +import io.xpipe.ext.proc.store.WslStore; +import io.xpipe.extension.test.LocalExtensionTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.List; + +public class CommandStoreTest extends LocalExtensionTest { + + @Test + public void testCmdRead() throws Exception { + var store = CommandStore.builder() + .host(ShellStore.local()) + .shell(ShellTypes.CMD) + .flow(DataFlow.INPUT) + .cmd("echo hi& echo there") + .build(); + try (InputStream inputStream = store.openInput()) { + var read = new String(inputStream.readAllBytes()); + Assertions.assertEquals("hi\r\nthere\r\n", read); + } + } + + @Test + public void testpowershellReadAndWritea() throws Exception { + try (ShellProcessControl pc = new WslStore(ShellStore.local(), null, null) + .create() + .subShell(ShellTypes.BASH) + .start()) { + try (var command = pc.command(List.of("echo", "hi")).start()) { + var read = command.readOnlyStdout(); + Assertions.assertEquals("hi", read); + } + + try (var command = pc.command(List.of("echo", "there")).start()) { + var read = command.readOnlyStdout(); + Assertions.assertEquals("there", read); + } + } + } + + @Test + public void testWslElevation() throws Exception { + try (ShellProcessControl pc = new WslStore(ShellStore.local(), null, null) + .create() + .subShell(ShellTypes.BASH) + .elevation(SecretValue.encrypt("123")) + .start()) { + try (var command = pc.command(List.of("echo", "hi")).elevated().start()) { + var read = command.readOnlyStdout(); + Assertions.assertEquals("hi", read); + } + } + } +} diff --git a/ext/proc/src/test/java/test/CommandTests.java b/ext/proc/src/test/java/test/CommandTests.java new file mode 100644 index 00000000..fcc32866 --- /dev/null +++ b/ext/proc/src/test/java/test/CommandTests.java @@ -0,0 +1,95 @@ +package test; + +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.extension.test.LocalExtensionTest; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import test.item.CommandCheckTestItem; +import test.item.ShellTestItem; + +import java.util.stream.Stream; + +public class CommandTests extends LocalExtensionTest { + + static Stream commandChecksProvider() { + Stream.Builder argumentBuilder = Stream.builder(); + for (var arg : ShellTestItem.getAll().toList()) { + for (var c : CommandCheckTestItem.values()) { + argumentBuilder.add(Arguments.of(arg, Named.of(c.name(), c))); + } + } + return argumentBuilder.build(); + } + + @ParameterizedTest + @MethodSource("commandChecksProvider") + public void testCommandChecks(ShellProcessControl shellTestItem, CommandCheckTestItem tc) throws Exception { + try (var pc = shellTestItem.start()) { + try (var c = pc.command(tc.getCommandFunction().apply(pc)).start()) { + tc.getCommandCheck().accept(c); + } + } + } + + @ParameterizedTest + @MethodSource("commandChecksProvider") + public void testDoubleCommandChecks(ShellProcessControl shellTestItem, CommandCheckTestItem tc) throws Exception { + try (var pc = shellTestItem.start()) { + try (var c = pc.command(tc.getCommandFunction().apply(pc)).start()) { + tc.getCommandCheck().accept(c); + } + + try (var c = pc.command(tc.getCommandFunction().apply(pc)).start()) { + tc.getCommandCheck().accept(c); + } + } + } + + @ParameterizedTest + @MethodSource("commandChecksProvider") + public void testSubCommandChecks(ShellProcessControl shellTestItem, CommandCheckTestItem tc) throws Exception { + try (var pc = shellTestItem.start()) { + try (ShellProcessControl sub = pc.subShell(pc.getShellType()).start()) { + try (var c = sub.command(tc.getCommandFunction().apply(sub)).start()) { + tc.getCommandCheck().accept(c); + } + } + } + } + + @ParameterizedTest + @MethodSource("commandChecksProvider") + public void testSubDoubleCommandChecks(ShellProcessControl shellTestItem, CommandCheckTestItem tc) throws Exception { + try (var pc = shellTestItem.start()) { + try (ShellProcessControl sub = pc.subShell(pc.getShellType()).start()) { + try (var c = sub.command(tc.getCommandFunction().apply(sub)).start()) { + tc.getCommandCheck().accept(c); + } + + try (var c = sub.command(tc.getCommandFunction().apply(sub)).start()) { + tc.getCommandCheck().accept(c); + } + } + } + } + + @ParameterizedTest + @MethodSource("commandChecksProvider") + public void testDoubleSubCommandChecks(ShellProcessControl shellTestItem, CommandCheckTestItem tc) throws Exception { + try (var pc = shellTestItem.start()) { + try (ShellProcessControl sub = pc.subShell(pc.getShellType()).start()) { + try (var c = sub.command(tc.getCommandFunction().apply(sub)).start()) { + tc.getCommandCheck().accept(c); + } + } + + try (ShellProcessControl sub = pc.subShell(pc.getShellType()).start()) { + try (var c = sub.command(tc.getCommandFunction().apply(sub)).start()) { + tc.getCommandCheck().accept(c); + } + } + } + } +} diff --git a/ext/proc/src/test/java/test/FailureTests.java b/ext/proc/src/test/java/test/FailureTests.java new file mode 100644 index 00000000..fe24164b --- /dev/null +++ b/ext/proc/src/test/java/test/FailureTests.java @@ -0,0 +1,83 @@ +package test; + +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.extension.test.LocalExtensionTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; + +public class FailureTests extends LocalExtensionTest { + + @ParameterizedTest + @MethodSource("test.item.ShellTestItem#getAll") + public void testFailedShellOpener(ShellProcessControl pc) throws Exception { + pc.start(); + var sub = pc.subShell( + pc.getShellType().executeCommandWithShell(pc.getShellType().getEchoCommand("hi", false))); + + Assertions.assertThrows(IOException.class, () -> { + sub.start(); + }); + + Assertions.assertFalse(pc.isRunning()); + Assertions.assertFalse(sub.isRunning()); + } + + @ParameterizedTest + @MethodSource("test.item.ShellTestItem#getAll") + public void testInvalidShellOpenerCommand(ShellProcessControl pc) throws Exception { + pc.start(); + var sub = pc.subShell("abc"); + + Assertions.assertThrows(IOException.class, () -> { + sub.start(); + }); + + Assertions.assertFalse(pc.isRunning()); + Assertions.assertFalse(sub.isRunning()); + } + + @ParameterizedTest + @MethodSource("test.item.ShellTestItem#getAll") + public void testLoopingRecoveryShellOpener(ShellProcessControl pc) throws Exception { + pc.start(); + var sub = pc.subShell("for i in {1..150}; do echo -n \"a\"; sleep 0.1s; done"); + + Assertions.assertThrows(IOException.class, () -> { + sub.start(); + }); + + Assertions.assertFalse(pc.isRunning()); + Assertions.assertFalse(sub.isRunning()); + } + + @ParameterizedTest + @MethodSource("test.item.ShellTestItem#getAll") + public void testLoopingShellOpener(ShellProcessControl pc) throws Exception { + pc.start(); + var sub = pc.subShell("for i in {1..150}; do echo hi; sleep 0.03s; done"); + + Assertions.assertThrows(IOException.class, () -> { + sub.start(); + }); + + Assertions.assertFalse(pc.isRunning()); + Assertions.assertFalse(sub.isRunning()); + } + + @ParameterizedTest + @MethodSource("test.item.ShellTestItem#getAll") + public void testFrozenShellOpener(ShellProcessControl pc) throws Exception { + pc.start(); + var sub = pc.subShell("sleep 30"); + + Assertions.assertThrows(IOException.class, () -> { + sub.start(); + }); + + Assertions.assertFalse(pc.isRunning()); + Assertions.assertFalse(sub.isRunning()); + } +} diff --git a/ext/proc/src/test/java/test/FileTest.java b/ext/proc/src/test/java/test/FileTest.java new file mode 100644 index 00000000..6085821e --- /dev/null +++ b/ext/proc/src/test/java/test/FileTest.java @@ -0,0 +1,56 @@ +package test; + +import io.xpipe.api.DataText; +import io.xpipe.core.impl.FileStore; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.process.ShellTypes; +import io.xpipe.core.store.FileSystemStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.extension.test.DaemonExtensionTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.UUID; + +public class FileTest extends DaemonExtensionTest { + + static DataText referenceFile; + + @BeforeAll + public static void setupStorage() throws Exception { + referenceFile = getSource("text", "utf8-bom-lf.txt").asText(); + } + + @ParameterizedTest + @MethodSource("test.item.ShellTestItem#getAll") + public void testReadAndWrite(ShellStore store) throws Exception { + try (var pc = store.create().start()) { + var file = getTestFile(pc); + var fileStore = FileStore.builder() + .fileSystem((FileSystemStore) store) + .file(file) + .build(); + var source = getSource("text", fileStore).asText(); + referenceFile.forwardTo(source); + + var read = source.readAll(); + + Assertions.assertEquals(referenceFile.readAll(), read); + } + } + + private String getTestFile(ShellProcessControl pc) throws Exception { + return getTemporaryDirectory(pc) + "/xpipe_test/" + UUID.randomUUID() + "/" + UUID.randomUUID() + ".txt"; + } + + private String getTemporaryDirectory(ShellProcessControl pc) throws Exception { + if (pc.getOsType().equals(OsType.WINDOWS)) { + return pc.executeStringSimpleCommand(ShellTypes.CMD, "echo %TEMP%"); + } + + return "/var/tmp"; + } +} diff --git a/ext/proc/src/test/java/test/ShellTests.java b/ext/proc/src/test/java/test/ShellTests.java new file mode 100644 index 00000000..fd4c7bec --- /dev/null +++ b/ext/proc/src/test/java/test/ShellTests.java @@ -0,0 +1,80 @@ +package test; + +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.process.ShellType; +import io.xpipe.extension.test.LocalExtensionTest; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import test.item.ShellCheckTestItem; +import test.item.ShellTestItem; + +import java.util.stream.Stream; + +public class ShellTests extends LocalExtensionTest { + + static Stream shellChecksProvider() { + Stream.Builder argumentBuilder = Stream.builder(); + for (var arg : ShellTestItem.getAll().toList()) { + for (var c : ShellCheckTestItem.values()) { + argumentBuilder.add(Arguments.of(arg, Named.of(c.name(), c))); + } + } + return argumentBuilder.build(); + } + + @ParameterizedTest + @MethodSource("shellChecksProvider") + public void testShellChecks(ShellProcessControl shellTestItem, ShellCheckTestItem ti) throws Exception { + try (var pc = shellTestItem.start()) { + ti.getShellCheck().accept(pc); + } + } + + @ParameterizedTest + @MethodSource("shellChecksProvider") + public void testDoubleShellChecks(ShellProcessControl shellTestItem, ShellCheckTestItem ti) throws Exception { + try (var pc = shellTestItem.start()) { + ti.getShellCheck().accept(pc); + pc.start(); + ti.getShellCheck().accept(pc); + } + } + + @ParameterizedTest + @MethodSource("shellChecksProvider") + public void testSubShellChecks(ShellProcessControl shellTestItem, ShellCheckTestItem ti) throws Exception { + try (var pc = shellTestItem.start()) { + try (ShellProcessControl sub = pc.subShell(pc.getShellType()).start()) { + ti.getShellCheck().accept(sub); + } + } + } + + @ParameterizedTest + @MethodSource("shellChecksProvider") + public void testSubDoubleShellChecks(ShellProcessControl shellTestItem, ShellCheckTestItem ti) throws Exception { + try (var pc = shellTestItem.start()) { + try (ShellProcessControl sub = pc.subShell(pc.getShellType()).start()) { + ti.getShellCheck().accept(sub); + sub.start(); + ti.getShellCheck().accept(sub); + } + } + } + + @ParameterizedTest + @MethodSource("shellChecksProvider") + public void testDoubleSubShellChecks(ShellProcessControl shellTestItem, ShellCheckTestItem ti) throws Exception { + try (var pc = shellTestItem.start()) { + ShellType t = pc.getShellType(); + try (ShellProcessControl sub = pc.subShell(t).start()) { + ti.getShellCheck().accept(sub); + } + try (ShellProcessControl sub = pc.subShell(t).start()) { + ti.getShellCheck().accept(sub); + } + } + } +} diff --git a/ext/proc/src/test/java/test/item/BasicShellTestItem.java b/ext/proc/src/test/java/test/item/BasicShellTestItem.java new file mode 100644 index 00000000..b7878350 --- /dev/null +++ b/ext/proc/src/test/java/test/item/BasicShellTestItem.java @@ -0,0 +1,34 @@ +package test.item; + +import io.xpipe.core.impl.LocalStore; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.process.ShellTypes; +import io.xpipe.ext.proc.store.ShellCommandStore; +import io.xpipe.extension.test.TestModule; + +import java.util.Map; +import java.util.function.Supplier; + +public class BasicShellTestItem extends TestModule { + + @Override + protected void init(Map> list) { + list.put("local", () -> new LocalStore().create()); + + if (OsType.getLocal().equals(OsType.WINDOWS)) { + list.put("local pwsh", () -> ShellCommandStore.shell(new LocalStore(), ShellTypes.POWERSHELL) + .create()); + list.put("local pwsh cmd", () -> ShellCommandStore.shell( + ShellCommandStore.shell( + ShellCommandStore.shell(new LocalStore(), ShellTypes.POWERSHELL), ShellTypes.CMD), + ShellTypes.CMD) + .create()); + } + } + + @Override + protected Class getValueClass() { + return ShellProcessControl.class; + } +} diff --git a/ext/proc/src/test/java/test/item/CommandCheckTestItem.java b/ext/proc/src/test/java/test/item/CommandCheckTestItem.java new file mode 100644 index 00000000..53803e49 --- /dev/null +++ b/ext/proc/src/test/java/test/item/CommandCheckTestItem.java @@ -0,0 +1,30 @@ +package test.item; + +import io.xpipe.core.process.CommandProcessControl; +import io.xpipe.core.process.ShellProcessControl; +import lombok.Getter; +import org.apache.commons.lang3.function.FailableConsumer; +import org.junit.jupiter.api.Assertions; + +import java.util.function.Function; + +@Getter +public enum CommandCheckTestItem { + ECHO( + shellProcessControl -> { + return shellProcessControl.getShellType().getEchoCommand("hi", false); + }, + commandProcessControl -> { + Assertions.assertEquals("hi", commandProcessControl.readOrThrow()); + }); + + private final Function commandFunction; + private final FailableConsumer commandCheck; + + CommandCheckTestItem( + Function commandFunction, + FailableConsumer commandCheck) { + this.commandFunction = commandFunction; + this.commandCheck = commandCheck; + } +} diff --git a/ext/proc/src/test/java/test/item/ShellCheckTestItem.java b/ext/proc/src/test/java/test/item/ShellCheckTestItem.java new file mode 100644 index 00000000..405f41cb --- /dev/null +++ b/ext/proc/src/test/java/test/item/ShellCheckTestItem.java @@ -0,0 +1,112 @@ +package test.item; + +import io.xpipe.core.impl.FileNames; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.ext.proc.util.ShellHelper; +import lombok.Getter; +import org.apache.commons.lang3.function.FailableConsumer; +import org.junit.jupiter.api.Assertions; + +import java.util.List; +import java.util.UUID; + +@Getter +public enum ShellCheckTestItem { + OS_NAME(shellProcessControl -> { + var os = ShellHelper.determineOsType(shellProcessControl); + os.determineOperatingSystemName(shellProcessControl); + }), + + RESTART(shellProcessControl -> { + var s1 = shellProcessControl.executeStringSimpleCommand("echo hi"); + Assertions.assertEquals("hi", s1); + shellProcessControl.restart(); + var s2 = shellProcessControl.executeStringSimpleCommand("echo world"); + Assertions.assertEquals("world", s2); + }), + + INIT_FILE(shellProcessControl -> { + var content = ""; + try (var c = shellProcessControl + .subShell(shellProcessControl.getShellType()) + .initWith(List.of(shellProcessControl.getShellType().getSetEnvironmentVariableCommand("testVar", content))) + .start()) { + var output = c.executeStringSimpleCommand( + shellProcessControl.getShellType().getPrintEnvironmentVariableCommand("testVar")); + Assertions.assertEquals(content, output); + } + }), + + STREAM_WRITE(shellProcessControl -> { + var content = "hello\nworldß"; + var fileOne = FileNames.join(shellProcessControl.getOsType().getTempDirectory(shellProcessControl), UUID.randomUUID().toString()); + try (var c = shellProcessControl + .command(shellProcessControl.getShellType().getStreamFileWriteCommand(fileOne)) + .start()) { + c.discardOut(); + c.discardErr(); + c.getStdin().write(content.getBytes(shellProcessControl.getCharset())); + c.closeStdin(); + } + + shellProcessControl.restart(); + + var fileTwo = FileNames.join(shellProcessControl.getOsType().getTempDirectory(shellProcessControl), UUID.randomUUID().toString()); + try (var c = shellProcessControl + .subShell(shellProcessControl.getShellType()) + .command(shellProcessControl.getShellType().getStreamFileWriteCommand(fileTwo)) + .start()) { + c.discardOut(); + c.discardErr(); + c.getStdin().write(content.getBytes(shellProcessControl.getCharset())); + c.closeStdin(); + } + + shellProcessControl.restart(); + + var s1 = shellProcessControl.executeStringSimpleCommand( + shellProcessControl.getShellType().getFileReadCommand(fileOne)); + var s2 = shellProcessControl.executeStringSimpleCommand( + shellProcessControl.getShellType().getFileReadCommand(fileTwo)); + Assertions.assertEquals(content, s1); + Assertions.assertEquals(content, s2); + }), + + SIMPLE_WRITE(shellProcessControl -> { + var content = "hello worldß"; + var fileOne = FileNames.join(shellProcessControl.getOsType().getTempDirectory(shellProcessControl), UUID.randomUUID().toString()); + shellProcessControl.executeSimpleCommand( + shellProcessControl.getShellType().getTextFileWriteCommand(content, fileOne)); + + var fileTwo = FileNames.join(shellProcessControl.getOsType().getTempDirectory(shellProcessControl), UUID.randomUUID().toString()); + shellProcessControl.executeSimpleCommand( + shellProcessControl.getShellType().getTextFileWriteCommand(content, fileTwo)); + + var s1 = shellProcessControl.executeStringSimpleCommand( + shellProcessControl.getShellType().getFileReadCommand(fileOne)); + var s2 = shellProcessControl.executeStringSimpleCommand( + shellProcessControl.getShellType().getFileReadCommand(fileTwo)); + Assertions.assertEquals(content, s1); + Assertions.assertEquals(content, s2); + }), + + TERMINAL_OPEN(shellProcessControl -> { + shellProcessControl.prepareIntermediateTerminalOpen(null); + }), + + COMMAND_TERMINAL_OPEN(shellProcessControl -> { + for (CommandCheckTestItem v : CommandCheckTestItem.values()) { + shellProcessControl.prepareIntermediateTerminalOpen(v.getCommandFunction().apply(shellProcessControl)); + } + }), + + ECHO(shellProcessControl -> { + ShellHelper.determineOsType(shellProcessControl); + }); + + private final FailableConsumer shellCheck; + + ShellCheckTestItem(FailableConsumer shellCheck) { + this.shellCheck = shellCheck; + } +} diff --git a/ext/proc/src/test/java/test/item/ShellTestItem.java b/ext/proc/src/test/java/test/item/ShellTestItem.java new file mode 100644 index 00000000..43547a2a --- /dev/null +++ b/ext/proc/src/test/java/test/item/ShellTestItem.java @@ -0,0 +1,14 @@ +package test.item; + +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.extension.test.TestModule; +import org.junit.jupiter.api.Named; + +import java.util.stream.Stream; + +public class ShellTestItem { + + public static Stream> getAll() { + return TestModule.getArguments(ShellProcessControl.class, "test.item.BasicShellTestItem", "test.item.PrivateShellTestItem"); + } +} diff --git a/ext/proc/src/test/resources/utf8-bom-lf.txt b/ext/proc/src/test/resources/utf8-bom-lf.txt new file mode 100644 index 00000000..5761cea7 --- /dev/null +++ b/ext/proc/src/test/resources/utf8-bom-lf.txt @@ -0,0 +1,2 @@ +hello +world \ No newline at end of file diff --git a/ext/procx/build.gradle b/ext/procx/build.gradle new file mode 100644 index 00000000..fce335e8 --- /dev/null +++ b/ext/procx/build.gradle @@ -0,0 +1 @@ +plugins { id 'java' } diff --git a/ext/procx/src/main/java/module-info.java b/ext/procx/src/main/java/module-info.java new file mode 100644 index 00000000..6eb3057c --- /dev/null +++ b/ext/procx/src/main/java/module-info.java @@ -0,0 +1 @@ +module io.xpipe.ext.procx {} diff --git a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java index e91c63ae..44afb998 100644 --- a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java @@ -79,7 +79,7 @@ public interface DataStoreProvider { } default String getModuleName() { - var n = getClass().getPackageName(); + var n = getClass().getModule().getName(); var i = n.lastIndexOf('.'); return i != -1 ? n.substring(i + 1) : n; } diff --git a/extension/src/main/java/io/xpipe/extension/XPipeServiceProviders.java b/extension/src/main/java/io/xpipe/extension/XPipeServiceProviders.java index ae10f8a6..db124a99 100644 --- a/extension/src/main/java/io/xpipe/extension/XPipeServiceProviders.java +++ b/extension/src/main/java/io/xpipe/extension/XPipeServiceProviders.java @@ -1,7 +1,7 @@ package io.xpipe.extension; import com.fasterxml.jackson.databind.jsontype.NamedType; -import io.xpipe.core.impl.ProcessControlProvider; +import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.ProxyFunction; import io.xpipe.extension.event.TrackEvent; diff --git a/extension/src/main/java/io/xpipe/extension/util/XPipeDistributionType.java b/extension/src/main/java/io/xpipe/extension/util/XPipeDistributionType.java index bcf1abb8..e70ac4a3 100644 --- a/extension/src/main/java/io/xpipe/extension/util/XPipeDistributionType.java +++ b/extension/src/main/java/io/xpipe/extension/util/XPipeDistributionType.java @@ -42,7 +42,7 @@ public interface XPipeDistributionType { @Override public boolean supportsURLs() { - return OsType.getLocal().equals(OsType.MAC); + return OsType.getLocal().equals(OsType.MACOS); } @Override