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 00000000..5e180c54 Binary files /dev/null and b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/cmd_icon.png differ diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/defaultShell_icon.png b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/defaultShell_icon.png new file mode 100644 index 00000000..9f75c310 Binary files /dev/null and b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/defaultShell_icon.png differ 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 00000000..95ad9e19 Binary files /dev/null and b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/docker_icon.png differ 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 00000000..54ec9b7a Binary files /dev/null and b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/shellCommand_icon.png differ diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/shellEnvironment_icon.png b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/shellEnvironment_icon.png new file mode 100644 index 00000000..1c528c7c Binary files /dev/null and b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/shellEnvironment_icon.png differ diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/sink_icon.png b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/sink_icon.png new file mode 100644 index 00000000..f63f6d82 Binary files /dev/null and b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/sink_icon.png differ 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 00000000..47ecd0d9 Binary files /dev/null and b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/ssh_icon.png differ diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/wsl_icon.png b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/wsl_icon.png new file mode 100644 index 00000000..518b5c79 Binary files /dev/null and b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/wsl_icon.png differ diff --git a/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/wsl_icon.svg b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/wsl_icon.svg new file mode 100644 index 00000000..a172ca97 --- /dev/null +++ b/ext/proc/src/main/resources/io/xpipe/ext/proc/resources/img/wsl_icon.svg @@ -0,0 +1,3409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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