diff --git a/app/src/main/java/io/xpipe/app/browser/action/ExecuteApplicationAction.java b/app/src/main/java/io/xpipe/app/browser/action/ExecuteApplicationAction.java index ae9b5db98..7153cc596 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/ExecuteApplicationAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/ExecuteApplicationAction.java @@ -2,7 +2,6 @@ package io.xpipe.app.browser.action; import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; -import io.xpipe.app.util.ApplicationHelper; import io.xpipe.core.process.ShellControl; import java.util.List; @@ -13,9 +12,7 @@ public abstract class ExecuteApplicationAction implements LeafAction, Applicatio public void execute(OpenFileSystemModel model, List entries) throws Exception { ShellControl sc = model.getFileSystem().getShell().orElseThrow(); for (BrowserEntry entry : entries) { - var command = detach() - ? ApplicationHelper.createDetachCommand(sc, createCommand(model, entry)) - : createCommand(model, entry); + var command = createCommand(model, entry); try (var cc = sc.command(command) .withWorkingDirectory(model.getCurrentDirectory().getPath()) .start()) { @@ -23,19 +20,11 @@ public abstract class ExecuteApplicationAction implements LeafAction, Applicatio } } - if (detach() && refresh()) { - throw new IllegalStateException(); - } - if (refresh()) { model.refreshSync(); } } - protected boolean detach() { - return false; - } - protected boolean refresh() { return false; } diff --git a/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java b/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java index 9ebf963f7..d5312186d 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java @@ -3,8 +3,8 @@ package io.xpipe.app.browser.action; import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.app.util.ApplicationHelper; import io.xpipe.app.util.TerminalLauncher; +import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellControl; import org.apache.commons.io.FilenameUtils; @@ -12,7 +12,7 @@ import java.util.List; public abstract class MultiExecuteAction implements BranchAction { - protected abstract String createCommand(ShellControl sc, OpenFileSystemModel model, BrowserEntry entry); + protected abstract CommandBuilder createCommand(ShellControl sc, OpenFileSystemModel model, BrowserEntry entry); @Override public List getBranchingActions(OpenFileSystemModel model, List entries) { @@ -56,9 +56,7 @@ public abstract class MultiExecuteAction implements BranchAction { model.withShell( pc -> { for (BrowserEntry entry : entries) { - var cmd = ApplicationHelper.createDetachCommand( - pc, createCommand(pc, model, entry)); - pc.command(cmd) + pc.command(createCommand(pc, model, entry)) .withWorkingDirectory(model.getCurrentDirectory() .getPath()) .execute(); @@ -71,27 +69,6 @@ public abstract class MultiExecuteAction implements BranchAction { public String getName(OpenFileSystemModel model, List entries) { return "in background"; } - }, - new LeafAction() { - - @Override - public void execute(OpenFileSystemModel model, List entries) { - model.withShell( - pc -> { - for (BrowserEntry entry : entries) { - pc.command(createCommand(pc, model, entry)) - .withWorkingDirectory(model.getCurrentDirectory() - .getPath()) - .execute(); - } - }, - false); - } - - @Override - public String getName(OpenFileSystemModel model, List entries) { - return "wait for completion"; - } }); } } diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java index 7dc847a27..50accbeb5 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java @@ -54,7 +54,7 @@ public class ErrorEvent { return EVENT_BASES.remove(t).description(msg); } - return builder().throwable(t).description(msg); + return builder().throwable(t).description(msg + (t.getMessage() != null ? "\n\n" + t.getMessage() : "")); } public static ErrorEventBuilder fromMessage(String msg) { diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index aef527c20..587bf31bb 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -11,7 +11,6 @@ import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.terminal.ExternalTerminalType; -import io.xpipe.app.util.ApplicationHelper; import io.xpipe.app.util.PasswordLockSecretValue; import io.xpipe.core.util.InPlaceSecretValue; import io.xpipe.core.util.ModuleHelper; @@ -503,7 +502,7 @@ public class AppPrefs { return null; } - return ApplicationHelper.replaceFileArgument(passwordManagerCommand.get(), "KEY", key); + return ExternalApplicationHelper.replaceFileArgument(passwordManagerCommand.get(), "KEY", key); } @Value diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationHelper.java b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationHelper.java new file mode 100644 index 000000000..2f3f5bf36 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationHelper.java @@ -0,0 +1,33 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.CommandBuilder; + +import java.util.Locale; + +public class ExternalApplicationHelper { + + public static String replaceFileArgument(String format, String variable, String file) { + // Support for legacy variables that were not upper case + variable = variable.toUpperCase(Locale.ROOT); + format = format.replace("$" + variable.toLowerCase(Locale.ROOT), "$" + variable.toUpperCase(Locale.ROOT)); + + var fileString = file.contains(" ") ? "\"" + file + "\"" : file; + // Check if the variable is already quoted + return format.replace("\"$" + variable + "\"", fileString).replace("$" + variable, fileString); + } + + public static void startAsync(CommandBuilder b) throws Exception { + try (var sc = LocalShell.getShell().start()) { + var cmd = sc.getShellDialect().launchAsnyc(b); + TrackEvent.withDebug("Executing local application") + .tag("command", b.buildFull(sc)) + .tag("adjusted", cmd.buildFull(sc)) + .handle(); + try (var c = sc.command(cmd).start()) { + c.discardOrThrow(); + } + } + } +} 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 62232138a..42db2800b 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java @@ -2,12 +2,11 @@ package io.xpipe.app.prefs; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.util.ApplicationHelper; +import io.xpipe.app.util.CommandSupport; import io.xpipe.app.util.LocalShell; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellControl; -import io.xpipe.core.process.ShellDialects; import java.io.IOException; import java.nio.file.Files; @@ -95,10 +94,12 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { public abstract static class PathApplication extends ExternalApplicationType { protected final String executable; + protected final boolean explicityAsync; - public PathApplication(String id, String executable) { + public PathApplication(String id, String executable, boolean explicityAsync) { super(id); this.executable = executable; + this.explicityAsync = explicityAsync; } public boolean isAvailable() { @@ -110,32 +111,21 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { } } - protected void launch(String title, String args) throws Exception { + protected void launch(String title, CommandBuilder args) throws Exception { try (ShellControl pc = LocalShell.getShell()) { - if (!ApplicationHelper.isInPath(pc, executable)) { + if (!CommandSupport.isInPath(pc, executable)) { throw ErrorEvent.expected( new IOException( "Executable " + executable + " not found in PATH. Either add it to the PATH and refresh the environment by restarting XPipe, or specify an absolute executable path using the custom terminal setting.")); } - if (ShellDialects.isPowershell(pc)) { - var cmd = CommandBuilder.of() - .add("Start-Process", "-FilePath") - .addFile(executable) - .add("-ArgumentList") - .add(pc.getShellDialect().literalArgument(args)); - pc.executeSimpleCommand(cmd); - return; - } - - var toExecute = executable + " " + args; - if (pc.getOsType().equals(OsType.WINDOWS)) { - toExecute = "start \"" + title + "\" " + toExecute; + args.add(0, executable); + if (explicityAsync) { + ExternalApplicationHelper.startAsync(args); } else { - toExecute = "nohup " + toExecute + " /dev/null & disown"; + pc.executeSimpleCommand(args); } - pc.executeSimpleCommand(toExecute); } } } 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 ea76403e3..a0a8f7b55 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -2,7 +2,7 @@ package io.xpipe.app.prefs; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.util.ApplicationHelper; +import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.WindowsRegistry; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.OsType; @@ -106,9 +106,8 @@ public interface ExternalEditorType extends PrefsChoiceValue { var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE"; - ApplicationHelper.executeLocalApplication( - CommandBuilder.of().add(ApplicationHelper.replaceFileArgument(format, "FILE", file.toString())), - true); + ExternalApplicationHelper.startAsync( + CommandBuilder.of().add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", file.toString()))); } @Override @@ -200,33 +199,28 @@ public interface ExternalEditorType extends PrefsChoiceValue { throw new IOException("Application " + applicationName + ".app not found"); } - ApplicationHelper.executeLocalApplication( + ExternalApplicationHelper.startAsync( CommandBuilder.of() .add("open", "-a") .addFile(execFile.orElseThrow().toString()) - .addFile(file.toString()), - false); + .addFile(file.toString())); } } class GenericPathType extends ExternalApplicationType.PathApplication implements ExternalEditorType { - private final boolean detach; - - public GenericPathType(String id, String command, boolean detach) { - super(id, command); - this.detach = detach; + public GenericPathType(String id, String command, boolean explicityAsync) { + super(id, command, explicityAsync); } @Override public void launch(Path file) throws Exception { - ApplicationHelper.executeLocalApplication( - CommandBuilder.of().add(executable).addFile(file.toString()), detach); - } - - @Override - public boolean isSelectable() { - return true; + var builder = CommandBuilder.of().addFile(executable).addFile(file.toString()); + if (explicityAsync) { + ExternalApplicationHelper.startAsync(builder); + } else { + LocalShell.getShell().executeSimpleCommand(builder); + } } } @@ -248,7 +242,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { public WindowsType(String id, String executable, boolean detach) { super(id, executable); - this.detach = detach; + this.detach = true; } @Override @@ -262,9 +256,12 @@ public interface ExternalEditorType extends PrefsChoiceValue { } } - Optional finalLocation = location; - ApplicationHelper.executeLocalApplication( - CommandBuilder.of().addFile(finalLocation.get().toString()).addFile(file.toString()), detach); + var builder = CommandBuilder.of().addFile(location.get().toString()).addFile(file.toString()); + if (detach) { + ExternalApplicationHelper.startAsync(builder); + } else { + LocalShell.getShell().executeSimpleCommand(builder); + } } } } diff --git a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java index 3b388ec48..df111b0a5 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java @@ -3,12 +3,10 @@ package io.xpipe.app.terminal; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.prefs.ExternalApplicationHelper; import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.storage.DataStoreColor; -import io.xpipe.app.util.ApplicationHelper; -import io.xpipe.app.util.LocalShell; -import io.xpipe.app.util.MacOsPermissions; -import io.xpipe.app.util.WindowsRegistry; +import io.xpipe.app.util.*; import io.xpipe.core.process.*; import io.xpipe.core.store.FilePath; import lombok.Getter; @@ -23,7 +21,7 @@ import java.util.function.Supplier; public interface ExternalTerminalType extends PrefsChoiceValue { - ExternalTerminalType CMD = new SimplePathType("app.cmd", "cmd.exe") { + ExternalTerminalType CMD = new SimplePathType("app.cmd", "cmd.exe", true) { @Override public boolean supportsTabs() { @@ -45,7 +43,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; - ExternalTerminalType POWERSHELL = new SimplePathType("app.powershell", "powershell") { + ExternalTerminalType POWERSHELL = new SimplePathType("app.powershell", "powershell", true) { @Override public boolean supportsTabs() { @@ -73,14 +71,14 @@ public interface ExternalTerminalType extends PrefsChoiceValue { var base64 = Base64.getEncoder() .encodeToString(configuration .getDialectLaunchCommand() - .buildCommandBase(sc) + .buildBase(sc) .getBytes(StandardCharsets.UTF_16LE)); return "\"" + base64 + "\""; }); } }; - ExternalTerminalType PWSH = new SimplePathType("app.pwsh", "pwsh") { + ExternalTerminalType PWSH = new SimplePathType("app.pwsh", "pwsh", true) { @Override public boolean supportsTabs() { @@ -100,14 +98,14 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .add(sc -> { // Fix for https://github.com/PowerShell/PowerShell/issues/18530#issuecomment-1325691850 var c = "$env:PSModulePath=\"\";" - + configuration.getDialectLaunchCommand().buildCommandBase(sc); + + configuration.getDialectLaunchCommand().buildBase(sc); var base64 = Base64.getEncoder().encodeToString(c.getBytes(StandardCharsets.UTF_16LE)); return "\"" + base64 + "\""; }); } }; - ExternalTerminalType ALACRITTY_WINDOWS = new SimplePathType("app.alacritty", "alacritty") { + ExternalTerminalType ALACRITTY_WINDOWS = new SimplePathType("app.alacritty", "alacritty", false) { @Override public boolean supportsTabs() { @@ -127,12 +125,13 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addQuoted("colors.primary.background='%s'" .formatted(configuration.getColor().toHexString())); } + + // Alacritty is bugged and will not accept arguments with spaces even if they are correctly passed/escaped + // So this will not work when the script file has spaces return b.add("-t") .addQuoted(configuration.getCleanTitle()) .add("-e") - .add("cmd") - .add("/c") - .addQuoted(configuration.getScriptFile().toString().replaceAll(" ", "^$0")); + .add(configuration.getDialectLaunchCommand()); } }; ExternalTerminalType TABBY_WINDOWS = new WindowsType("app.tabby", "Tabby") { @@ -145,11 +144,20 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override protected void execute(Path file, LaunchConfiguration configuration) throws Exception { // Tabby has a very weird handling of output, even detaching with start does not prevent it from printing + if (configuration.getScriptDialect().equals(ShellDialects.CMD)) { + // It also freezes with any other input than .bat files, why? + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of() + .addFile(file.toString()) + .add("run") + .addFile(configuration.getScriptFile()) + .discardOutput()); + } + LocalShell.getShell() .executeSimpleCommand(CommandBuilder.of() .addFile(file.toString()) .add("run") - .addFile(configuration.getScriptFile()) + .add(configuration.getDialectLaunchCommand()) .discardOutput()); } @@ -183,9 +191,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override protected void execute(Path file, LaunchConfiguration configuration) throws Exception { - ApplicationHelper.executeLocalApplication( - CommandBuilder.of().addFile(file.toString()).add("start").addFile(configuration.getScriptFile()), - true); + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().addFile(file.toString()).add("start").add(configuration.getDialectLaunchCommand())); } @Override @@ -199,7 +205,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { return launcherDir.map(Path::of); } }; - ExternalTerminalType WEZ_LINUX = new SimplePathType("app.wezterm", "wezterm-gui") { + ExternalTerminalType WEZ_LINUX = new SimplePathType("app.wezterm", "wezterm-gui", true) { @Override public boolean supportsTabs() { @@ -211,7 +217,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { return CommandBuilder.of().add("start").addFile(configuration.getScriptFile()); } }; - ExternalTerminalType GNOME_TERMINAL = new PathCheckType("app.gnomeTerminal", "gnome-terminal") { + ExternalTerminalType GNOME_TERMINAL = new PathCheckType("app.gnomeTerminal", "gnome-terminal", true) { @Override public boolean supportsTabs() { @@ -221,7 +227,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override public void launch(LaunchConfiguration configuration) throws Exception { try (ShellControl pc = LocalShell.getShell()) { - ApplicationHelper.checkIsInPath( + CommandSupport.isInPathOrThrow( pc, executable, toTranslatedString().getValue(), null); var toExecute = CommandBuilder.of() @@ -229,15 +235,13 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addQuoted(configuration.getColoredTitle()) .add("--") .addFile(configuration.getScriptFile()) - .buildString(pc); - // In order to fix this bug which also affects us: - // https://askubuntu.com/questions/1148475/launching-gnome-terminal-from-vscode - toExecute = "GNOME_TERMINAL_SCREEN=\"\" nohup " + toExecute + " /dev/null & disown"; - pc.executeSimpleCommand(toExecute); + // In order to fix this bug which also affects us: + // https://askubuntu.com/questions/1148475/launching-gnome-terminal-from-vscode + .envrironment("GNOME_TERMINAL_SCREEN", sc -> ""); } } }; - ExternalTerminalType KONSOLE = new SimplePathType("app.konsole", "konsole") { + ExternalTerminalType KONSOLE = new SimplePathType("app.konsole", "konsole", true) { @Override public boolean supportsTabs() { @@ -257,7 +261,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { return CommandBuilder.of().add("--new-tab", "-e").addFile(configuration.getScriptFile()); } }; - ExternalTerminalType XFCE = new SimplePathType("app.xfce", "xfce4-terminal") { + ExternalTerminalType XFCE = new SimplePathType("app.xfce", "xfce4-terminal", true) { @Override public boolean supportsTabs() { @@ -273,7 +277,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addFile(configuration.getScriptFile()); } }; - ExternalTerminalType ELEMENTARY = new SimplePathType("app.elementaryTerminal", "io.elementary.terminal") { + ExternalTerminalType ELEMENTARY = new SimplePathType("app.elementaryTerminal", "io.elementary.terminal", true) { @Override public boolean supportsTabs() { @@ -285,7 +289,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { return CommandBuilder.of().add("--new-tab").add("-e").addFile(configuration.getColoredTitle()); } }; - ExternalTerminalType TILIX = new SimplePathType("app.tilix", "tilix") { + ExternalTerminalType TILIX = new SimplePathType("app.tilix", "tilix", true) { @Override public boolean supportsTabs() { @@ -301,7 +305,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addFile(configuration.getScriptFile()); } }; - ExternalTerminalType TERMINATOR = new SimplePathType("app.terminator", "terminator") { + ExternalTerminalType TERMINATOR = new SimplePathType("app.terminator", "terminator", true) { @Override public boolean supportsTabs() { @@ -318,7 +322,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .add("--new-tab"); } }; - ExternalTerminalType TERMINOLOGY = new SimplePathType("app.terminology", "terminology") { + ExternalTerminalType TERMINOLOGY = new SimplePathType("app.terminology", "terminology", true) { @Override public boolean supportsTabs() { @@ -335,7 +339,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addFile(configuration.getScriptFile()); } }; - ExternalTerminalType COOL_RETRO_TERM = new SimplePathType("app.coolRetroTerm", "cool-retro-term") { + ExternalTerminalType COOL_RETRO_TERM = new SimplePathType("app.coolRetroTerm", "cool-retro-term", true) { @Override public boolean supportsTabs() { @@ -351,7 +355,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addFile(configuration.getScriptFile()); } }; - ExternalTerminalType GUAKE = new SimplePathType("app.guake", "guake") { + ExternalTerminalType GUAKE = new SimplePathType("app.guake", "guake", true) { @Override public boolean supportsTabs() { @@ -368,7 +372,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addFile(configuration.getScriptFile()); } }; - ExternalTerminalType ALACRITTY_LINUX = new SimplePathType("app.alacritty", "alacritty") { + ExternalTerminalType ALACRITTY_LINUX = new SimplePathType("app.alacritty", "alacritty", true) { @Override public boolean supportsTabs() { @@ -389,7 +393,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addFile(configuration.getScriptFile()); } }; - ExternalTerminalType TILDA = new SimplePathType("app.tilda", "tilda") { + ExternalTerminalType TILDA = new SimplePathType("app.tilda", "tilda", true) { @Override public boolean supportsTabs() { @@ -401,7 +405,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { return CommandBuilder.of().add("-c").addFile(configuration.getScriptFile()); } }; - ExternalTerminalType XTERM = new SimplePathType("app.xterm", "xterm") { + ExternalTerminalType XTERM = new SimplePathType("app.xterm", "xterm", true) { @Override public boolean supportsTabs() { @@ -417,7 +421,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addFile(configuration.getScriptFile()); } }; - ExternalTerminalType DEEPIN_TERMINAL = new SimplePathType("app.deepinTerminal", "deepin-terminal") { + ExternalTerminalType DEEPIN_TERMINAL = new SimplePathType("app.deepinTerminal", "deepin-terminal", true) { @Override public boolean supportsTabs() { @@ -429,7 +433,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { return CommandBuilder.of().add("-C").addFile(configuration.getScriptFile()); } }; - ExternalTerminalType Q_TERMINAL = new SimplePathType("app.qTerminal", "qterminal") { + ExternalTerminalType Q_TERMINAL = new SimplePathType("app.qTerminal", "qterminal", true) { @Override public boolean supportsTabs() { @@ -576,10 +580,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .resolve("wezterm-gui") .toString()) .add("start") - .addFile(configuration.getScriptFile()) - .buildString(LocalShell.getShell()); - c = ApplicationHelper.createDetachCommand(LocalShell.getShell(), c); - LocalShell.getShell().executeSimpleCommand(c); + .add(configuration.getDialectLaunchCommand()); + ExternalApplicationHelper.startAsync(c); } }; ExternalTerminalType KITTY_MACOS = new MacOsType("app.kitty", "kitty") { @@ -768,7 +770,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { var format = custom.toLowerCase(Locale.ROOT).contains("$cmd") ? custom : custom + " $CMD"; try (var pc = LocalShell.getShell()) { - var toExecute = ApplicationHelper.replaceFileArgument(format, "CMD", configuration.getScriptFile().toString()); + var toExecute = ExternalApplicationHelper.replaceFileArgument(format, "CMD", configuration.getScriptFile().toString()); // We can't be sure whether the command is blocking or not, so always make it not blocking if (pc.getOsType().equals(OsType.WINDOWS)) { toExecute = "start \"" + configuration.getCleanTitle() + "\" " + toExecute; @@ -795,21 +797,21 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Getter abstract class PathCheckType extends ExternalApplicationType.PathApplication implements ExternalTerminalType { - public PathCheckType(String id, String executable) { - super(id, executable); + public PathCheckType(String id, String executable, boolean explicitAsync) { + super(id, executable, explicitAsync); } } @Getter abstract class SimplePathType extends PathCheckType { - public SimplePathType(String id, String executable) { - super(id, executable); + public SimplePathType(String id, String executable, boolean explicitAsync) { + super(id, executable, explicitAsync); } @Override public void launch(LaunchConfiguration configuration) throws Exception { - var args = toCommand(configuration).buildCommandBase(LocalShell.getShell()); + var args = toCommand(configuration); launch(configuration.getColoredTitle(), args); } diff --git a/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java index 96f63e753..656a3bc33 100644 --- a/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java @@ -1,7 +1,7 @@ package io.xpipe.app.terminal; import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import io.xpipe.app.util.ApplicationHelper; +import io.xpipe.app.util.CommandSupport; import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.ShellTemp; import io.xpipe.app.util.ThreadHelper; @@ -44,8 +44,8 @@ public class KittyTerminalType { private static boolean prepare() throws Exception { var socket = getSocket(); try (var sc = LocalShell.getShell().start()) { - ApplicationHelper.checkIsInPath(sc, "kitty", "Kitty", null); - ApplicationHelper.checkIsInPath(sc, "socat", "socat", null); + CommandSupport.isInPathOrThrow(sc, "kitty", "Kitty", null); + CommandSupport.isInPathOrThrow(sc, "socat", "socat", null); if (sc.executeSimpleBooleanCommand("test -w " + sc.getShellDialect().fileArgument(socket))) { return false; diff --git a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java index 037211a02..032a966b1 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java @@ -11,7 +11,7 @@ import java.nio.file.Path; public class WindowsTerminalType { public static final ExternalTerminalType WINDOWS_TERMINAL = - new ExternalTerminalType.SimplePathType("app.windowsTerminal", "wt.exe") { + new ExternalTerminalType.SimplePathType("app.windowsTerminal", "wt.exe", false) { @Override protected CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception { diff --git a/app/src/main/java/io/xpipe/app/util/ApplicationHelper.java b/app/src/main/java/io/xpipe/app/util/ApplicationHelper.java deleted file mode 100644 index de685092b..000000000 --- a/app/src/main/java/io/xpipe/app/util/ApplicationHelper.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.xpipe.app.util; - -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.issue.TrackEvent; -import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.core.process.CommandBuilder; -import io.xpipe.core.process.OsType; -import io.xpipe.core.process.ShellControl; -import io.xpipe.core.process.ShellDialects; -import io.xpipe.core.util.FailableSupplier; - -import java.io.IOException; -import java.util.Locale; - -public class ApplicationHelper { - - public static String replaceFileArgument(String format, String variable, String file) { - // Support for legacy variables that were not upper case - variable = variable.toUpperCase(Locale.ROOT); - format = format.replace("$" + variable.toLowerCase(Locale.ROOT), "$" + variable.toUpperCase(Locale.ROOT)); - - var fileString = file.contains(" ") ? "\"" + file + "\"" : file; - // Check if the variable is already quoted - return format.replace("\"$" + variable + "\"", fileString).replace("$" + variable, fileString); - } - - public static void executeLocalApplication(CommandBuilder b, boolean detach) throws Exception { - try (var sc = LocalShell.getShell().start()) { - var cmd = detach ? createDetachCommand(sc, b.buildString(sc)) : b.buildString(sc); - TrackEvent.withDebug("Executing local application") - .tag("command", cmd) - .handle(); - try (var c = sc.command(cmd).start()) { - c.discardOrThrow(); - } - } - } - - public static String createDetachCommand(ShellControl pc, String command) { - if (ShellDialects.isPowershell(pc)) { - var script = ScriptHelper.createExecScript(pc, command); - return String.format( - "Start-Process -FilePath %s -ArgumentList \"-NoProfile\", \"-File\", %s", - pc.getShellDialect().getExecutableName(), - pc.getShellDialect().fileArgument(script.toString())); - } - - if (pc.getOsType().equals(OsType.WINDOWS)) { - return "start \"\" " + command; - } else { - return "nohup " + command + " /dev/null & disown"; - } - } - - public static boolean isInPath(ShellControl processControl, String executable) throws Exception { - return processControl.executeSimpleBooleanCommand( - processControl.getShellDialect().getWhichCommand(executable)); - } - - public static boolean isInPathSilent(ShellControl processControl, String executable) { - try { - return processControl.executeSimpleBooleanCommand( - processControl.getShellDialect().getWhichCommand(executable)); - } catch (Exception e) { - ErrorEvent.fromThrowable(e).handle(); - return false; - } - } - - public static void checkIsInPath( - ShellControl processControl, String executable, String displayName, DataStoreEntry connection) - throws Exception { - if (!isInPath(processControl, executable)) { - throw ErrorEvent.expected(new IOException(displayName + " executable " + executable + " not found in PATH" - + (connection != null ? " on system " + connection.getName() : ""))); - } - } - - public static void isSupported(FailableSupplier supplier, String displayName, DataStoreEntry connection) - throws Exception { - if (!supplier.get()) { - throw ErrorEvent.expected(new IOException(displayName + " is not supported" - + (connection != null ? " on system " + connection.getName() : ""))); - } - } -} diff --git a/app/src/main/java/io/xpipe/app/util/CommandSupport.java b/app/src/main/java/io/xpipe/app/util/CommandSupport.java new file mode 100644 index 000000000..b1b04b2d6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/CommandSupport.java @@ -0,0 +1,42 @@ +package io.xpipe.app.util; + +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.util.FailableSupplier; + +import java.io.IOException; + +public class CommandSupport { + public static boolean isInPath(ShellControl processControl, String executable) throws Exception { + return processControl.executeSimpleBooleanCommand( + processControl.getShellDialect().getWhichCommand(executable)); + } + + public static boolean isInPathSilent(ShellControl processControl, String executable) { + try { + return processControl.executeSimpleBooleanCommand( + processControl.getShellDialect().getWhichCommand(executable)); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).handle(); + return false; + } + } + + public static void isInPathOrThrow( + ShellControl processControl, String executable, String displayName, DataStoreEntry connection) + throws Exception { + if (!isInPath(processControl, executable)) { + throw ErrorEvent.expected(new IOException(displayName + " executable " + executable + " not found in PATH" + + (connection != null ? " on system " + connection.getName() : ""))); + } + } + + public static void isSupported(FailableSupplier supplier, String displayName, DataStoreEntry connection) + throws Exception { + if (!supplier.get()) { + throw ErrorEvent.expected(new IOException(displayName + " is not supported" + + (connection != null ? " on system " + connection.getName() : ""))); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/util/ShellControlCache.java b/app/src/main/java/io/xpipe/app/util/ShellControlCache.java index ee044a125..37d7e4b57 100644 --- a/app/src/main/java/io/xpipe/app/util/ShellControlCache.java +++ b/app/src/main/java/io/xpipe/app/util/ShellControlCache.java @@ -46,7 +46,7 @@ public class ShellControlCache { public boolean isApplicationInPath(String app) { if (!installedApplications.containsKey(app)) { try { - var b = ApplicationHelper.isInPath(shellControl, app); + var b = CommandSupport.isInPath(shellControl, app); installedApplications.put(app, b); } catch (Exception e) { installedApplications.put(app, false); diff --git a/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java b/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java index 357a54e3c..4ee4a3209 100644 --- a/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java +++ b/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java @@ -61,9 +61,7 @@ public class TerminalLauncher { latch.await(); } catch (Exception ex) { throw ErrorEvent.expected(new IOException( - "Unable to launch terminal " + type.toTranslatedString().getValue() + ": " + ex.getMessage() - + ".\nMaybe try to use a different terminal in the settings.", - ex)); + "Unable to launch terminal " + type.toTranslatedString().getValue() + ": " + ex.getMessage(), ex)); } } } diff --git a/core/src/main/java/io/xpipe/core/process/CommandBuilder.java b/core/src/main/java/io/xpipe/core/process/CommandBuilder.java index 4da59cb6d..4e4f859bf 100644 --- a/core/src/main/java/io/xpipe/core/process/CommandBuilder.java +++ b/core/src/main/java/io/xpipe/core/process/CommandBuilder.java @@ -210,7 +210,11 @@ public class CommandBuilder { return this; } - public String buildCommandBase(ShellControl sc) throws Exception { + public String buildBase(ShellControl sc) throws Exception { + return String.join(" ", buildBaseParts(sc)); + } + + public List buildBaseParts(ShellControl sc) throws Exception { countDown = CountDown.of(); uuid = UUID.randomUUID(); @@ -227,11 +231,11 @@ public class CommandBuilder { list.add(evaluate); } - return String.join(" ", list); + return list; } - public String buildString(ShellControl sc) throws Exception { - var s = buildCommandBase(sc); + public String buildFull(ShellControl sc) throws Exception { + var s = buildBase(sc); LinkedHashMap map = new LinkedHashMap<>(); for (var e : environmentVariables.entrySet()) { var v = e.getValue().evaluate(sc); @@ -239,7 +243,7 @@ public class CommandBuilder { map.put(e.getKey(), v); } } - return sc.getShellDialect().addInlineVariablesToCommand(map, s); + return sc.getShellDialect().assembleCommand(s, map); } public CommandControl build(ShellControl sc) { diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialect.java b/core/src/main/java/io/xpipe/core/process/ShellDialect.java index b63686940..3dae68186 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java @@ -16,6 +16,8 @@ import java.util.stream.Stream; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface ShellDialect { + CommandBuilder launchAsnyc(CommandBuilder cmd); + default String getLicenseFeatureId() { return null; } @@ -80,7 +82,7 @@ public interface ShellDialect { String getScriptFileEnding(); - String addInlineVariablesToCommand(Map variables, String command); + String assembleCommand(String command, Map variables); Stream listFiles(FileSystem fs, ShellControl control, String dir) throws Exception; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java index c8191622d..b8c10c902 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java @@ -5,6 +5,7 @@ import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.action.BrowserActionFormatter; import io.xpipe.app.browser.action.MultiExecuteAction; import io.xpipe.app.browser.icon.BrowserIconFileType; +import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellControl; import java.util.List; @@ -27,8 +28,8 @@ public class JarAction extends MultiExecuteAction implements JavaAction, FileTyp } @Override - protected String createCommand(ShellControl sc, OpenFileSystemModel model, BrowserEntry entry) { - return "java -jar " + entry.getOptionallyQuotedFileName(); + protected CommandBuilder createCommand(ShellControl sc, OpenFileSystemModel model, BrowserEntry entry) { + return CommandBuilder.of().add("java", "-jar").addFile(entry.getRawFileEntry().getPath()); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java index ca24435ac..70f79f750 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java @@ -3,6 +3,7 @@ package io.xpipe.ext.base.browser; import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.action.MultiExecuteAction; +import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellDialects; @@ -69,8 +70,7 @@ public class RunAction extends MultiExecuteAction { return entries.stream().allMatch(entry -> isExecutable(entry.getRawFileEntry())); } - @Override - protected String createCommand(ShellControl sc, OpenFileSystemModel model, BrowserEntry entry) { - return sc.getShellDialect().runScriptCommand(sc, entry.getRawFileEntry().getPath()); + protected CommandBuilder createCommand(ShellControl sc, OpenFileSystemModel model, BrowserEntry entry) { + return CommandBuilder.of().add(sc.getShellDialect().runScriptCommand(sc, entry.getRawFileEntry().getPath())); } }