diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java index 9d05484a6..9ff60fffe 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java @@ -36,6 +36,7 @@ public class ShellStartExchangeImpl extends ShellStartExchange { .osType(control.getOsType()) .osName(control.getOsName()) .temp(control.getSystemTemporaryDirectory()) + .ttyState(control.getTtyState()) .build(); } } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java index fe74fb481..9a5bb8981 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java @@ -40,7 +40,7 @@ public class StoreCreationMenu { menu.getItems() .add(category("addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, null)); - // menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, "cmd")); + menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, "cmd")); menu.getItems().add(category("addSerial", "mdi2s-serial-port", DataStoreCreationCategory.SERIAL, "serial")); diff --git a/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java b/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java index d94a2a2fa..30988d1fc 100644 --- a/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java +++ b/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java @@ -6,6 +6,7 @@ import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.core.process.ShellDialects; import io.xpipe.core.process.ShellStoreState; +import io.xpipe.core.process.ShellTtyState; import javafx.beans.value.ObservableValue; public class DataStoreFormatter { @@ -41,7 +42,8 @@ public class DataStoreFormatter { return s.getShellDialect().getDisplayName(); } - return s.isRunning() ? formattedOsName(s.getOsName()) : "Connection failed"; + var prefix = s.getTtyState() != ShellTtyState.NONE ? "[PTY] " : ""; + return s.isRunning() ? prefix + formattedOsName(s.getOsName()) : "Connection failed"; } return "?"; diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/api.md b/app/src/main/resources/io/xpipe/app/resources/misc/api.md index e0361cee1..0fb4a6f37 100644 --- a/app/src/main/resources/io/xpipe/app/resources/misc/api.md +++ b/app/src/main/resources/io/xpipe/app/resources/misc/api.md @@ -1673,6 +1673,7 @@ These errors will be returned with the HTTP return code 500. "shellDialect": 0, "osType": "string", "osName": "string", + "ttyState": "string", "temp": "string" } ``` @@ -2969,6 +2970,7 @@ undefined "shellDialect": 0, "osType": "string", "osName": "string", + "ttyState": "string", "temp": "string" } @@ -2981,6 +2983,7 @@ undefined |shellDialect|integer|true|none|The shell dialect| |osType|string|true|none|The general type of operating system| |osName|string|true|none|The display name of the operating system| +|ttyState|string|false|none|Whether a tty/pty has been allocated for the connection. If allocated, input and output will be unreliable. It is not recommended to use a shell connection then.| |temp|string|true|none|The location of the temporary directory|

ShellStopRequest

diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java index 10f32b12b..4d4115e29 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java @@ -3,6 +3,7 @@ package io.xpipe.beacon.api; import io.xpipe.beacon.BeaconInterface; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellDialect; +import io.xpipe.core.process.ShellTtyState; import io.xpipe.core.store.FilePath; import lombok.Builder; @@ -40,6 +41,9 @@ public class ShellStartExchange extends BeaconInterface getProperties(ShellControl pc) throws Exception; - - String determineOperatingSystemName(ShellControl pc) throws Exception; - sealed interface Local extends OsType permits OsType.Windows, OsType.Linux, OsType.MacOs { String getId(); @@ -87,51 +79,6 @@ public interface OsType { return "Windows"; } - @Override - public String getTempDirectory(ShellControl pc) throws Exception { - var def = pc.executeSimpleStringCommand(pc.getShellDialect().getPrintEnvironmentVariableCommand("TEMP")); - if (!def.isBlank() && pc.getShellDialect().directoryExists(pc, def).executeAndCheck()) { - return def; - } - - var fallback = pc.executeSimpleStringCommand( - pc.getShellDialect().getPrintEnvironmentVariableCommand("LOCALAPPDATA")); - if (!fallback.isBlank() - && pc.getShellDialect().directoryExists(pc, fallback).executeAndCheck()) { - return fallback; - } - - return def; - } - - @Override - public Map getProperties(ShellControl pc) throws Exception { - try (CommandControl c = pc.command("systeminfo").start()) { - var text = c.readStdoutOrThrow(); - return PropertiesFormatsParser.parse(text, ":"); - } - } - - @Override - public String determineOperatingSystemName(ShellControl pc) { - try { - return pc.executeSimpleStringCommand("wmic os get Caption") - .lines() - .skip(1) - .collect(Collectors.joining()) - .trim() - + " " - + pc.executeSimpleStringCommand("wmic os get Version") - .lines() - .skip(1) - .collect(Collectors.joining()) - .trim(); - } catch (Throwable t) { - // Just in case this fails somehow - return "Windows"; - } - } - @Override public String getId() { return "windows"; @@ -168,32 +115,6 @@ public interface OsType { return "Linux"; } - @Override - public String getTempDirectory(ShellControl pc) { - return "/tmp/"; - } - - @Override - public Map getProperties(ShellControl pc) { - return null; - } - - @Override - public String determineOperatingSystemName(ShellControl pc) throws Exception { - String type = "Unknown"; - var uname = pc.command("uname -o").readStdoutIfPossible(); - if (uname.isPresent()) { - type = uname.get(); - } - - String version = "?"; - var unameR = pc.command("uname -r").readStdoutIfPossible(); - if (unameR.isPresent()) { - version = unameR.get(); - } - - return type + " " + version; - } } final class Linux extends Unix implements OsType, Local, Any { @@ -203,20 +124,6 @@ public interface OsType { return "linux"; } - @Override - public String determineOperatingSystemName(ShellControl pc) throws Exception { - var rel = pc.command("lsb_release -a").readStdoutIfPossible(); - if (rel.isPresent()) { - return PropertiesFormatsParser.parse(rel.get(), ":").getOrDefault("Description", "Unknown"); - } - - var cat = pc.command("cat /etc/*release").readStdoutIfPossible(); - if (cat.isPresent()) { - return PropertiesFormatsParser.parse(cat.get(), "=").getOrDefault("PRETTY_NAME", "Unknown"); - } - - return super.determineOperatingSystemName(pc); - } } final class Solaris extends Unix implements Any {} @@ -265,38 +172,5 @@ public interface OsType { return "Mac"; } - @Override - public String getTempDirectory(ShellControl pc) throws Exception { - var found = pc.executeSimpleStringCommand(pc.getShellDialect().getPrintVariableCommand("TMPDIR")); - - // This variable is not defined for root users, so manually fix it. Why? ... - if (found.isBlank()) { - return "/tmp"; - } - - return found; - } - - @Override - public Map getProperties(ShellControl pc) throws Exception { - try (CommandControl c = pc.command("sw_vers").start()) { - var text = c.readStdoutOrThrow(); - return PropertiesFormatsParser.parse(text, ":"); - } - } - - @Override - public String determineOperatingSystemName(ShellControl pc) throws Exception { - var properties = getProperties(pc); - var name = pc.executeSimpleStringCommand( - "awk '/SOFTWARE LICENSE AGREEMENT FOR macOS/' '/System/Library/CoreServices/Setup " - + "Assistant.app/Contents/Resources/en.lproj/OSXSoftwareLicense.rtf' | " - + "awk -F 'macOS ' '{print $NF}' | awk '{print substr($0, 0, length($0)-1)}'"); - // For preleases and others - if (name.isBlank()) { - name = "?"; - } - return properties.get("ProductName") + " " + name + " " + properties.get("ProductVersion"); - } } } 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 ff61b7b21..943c4931e 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java @@ -9,6 +9,7 @@ import io.xpipe.core.util.StreamCharset; import java.nio.charset.Charset; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; @@ -120,6 +121,8 @@ public interface ShellDialect { String getPrintStartEchoCommand(String prefix); + Optional executeRobustBootstrapOutputCommand(ShellControl shellControl, String original) throws Exception; + String getPrintExitCodeCommand(String id, String prefix, String suffix); int assignMissingExitCode(); diff --git a/core/src/main/java/io/xpipe/core/process/ShellTtyState.java b/core/src/main/java/io/xpipe/core/process/ShellTtyState.java index d27745c57..ef1afe101 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellTtyState.java +++ b/core/src/main/java/io/xpipe/core/process/ShellTtyState.java @@ -7,19 +7,23 @@ import lombok.Getter; public enum ShellTtyState { @JsonProperty("none") - NONE(true, false, false), + NONE(true, false, false, true, true), @JsonProperty("merged") - MERGED_STDERR(false, false, false), + MERGED_STDERR(false, false, false, false, true), @JsonProperty("pty") - PTY_ALLOCATED(false, true, true); + PTY_ALLOCATED(false, true, true, false, false); private final boolean hasSeparateStreams; private final boolean hasAnsiEscapes; private final boolean echoesAllInput; + private final boolean supportsInput; + private final boolean preservesOutput; - ShellTtyState(boolean hasSeparateStreams, boolean hasAnsiEscapes, boolean echoesAllInput) { + ShellTtyState(boolean hasSeparateStreams, boolean hasAnsiEscapes, boolean echoesAllInput, boolean supportsInput, boolean preservesOutput) { this.hasSeparateStreams = hasSeparateStreams; this.hasAnsiEscapes = hasAnsiEscapes; this.echoesAllInput = echoesAllInput; + this.supportsInput = supportsInput; + this.preservesOutput = preservesOutput; } } diff --git a/dist/changelogs/11.0.md b/dist/changelogs/11.0.md index 3dc3e1336..f177be8e7 100644 --- a/dist/changelogs/11.0.md +++ b/dist/changelogs/11.0.md @@ -1,3 +1,11 @@ +## TTYs and PTYs + +Up until now, if you added a connection that always allocated pty, XPipe would complain about a missing stderr. +In XPipe 11, there has been a ground up rework of the shell initialization code which will in theory allow for better handling of these cases. +They are not fully supported yet and have some issues, but should work better. + +The main concern here is to verify that the existing normal shell implementation still works as before and there were no bugs introduced by this rework. + ## Profiles You can now create multiple user profiles in the settings menu. diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java index 1ee97a740..53e6535ba 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java @@ -4,7 +4,6 @@ import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.comp.base.OsLogoComp; import io.xpipe.app.comp.base.SystemStateComp; import io.xpipe.app.comp.base.TtyWarningComp; -import io.xpipe.app.comp.store.StoreEntryComp; import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.comp.store.StoreSection; import io.xpipe.app.ext.ActionProvider; @@ -13,6 +12,7 @@ import io.xpipe.app.ext.DataStoreUsageCategory; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.DataStoreFormatter; import io.xpipe.app.util.TerminalLauncher; import io.xpipe.core.process.ShellStoreState; import io.xpipe.core.process.ShellTtyState; @@ -20,6 +20,7 @@ import io.xpipe.core.store.ShellStore; import io.xpipe.ext.base.script.ScriptStore; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; +import javafx.beans.value.ObservableValue; public interface ShellStoreProvider extends DataStoreProvider { @@ -32,11 +33,6 @@ public interface ShellStoreProvider extends DataStoreProvider { w.getPersistentState())); } - @Override - default StoreEntryComp customEntryComp(StoreSection s, boolean preferLarge) { - return StoreEntryComp.create(s, createTtyWarning(s.getWrapper()), preferLarge); - } - @Override default ActionProvider.Action launchAction(DataStoreEntry entry) { return new ActionProvider.Action() { @@ -66,4 +62,9 @@ public interface ShellStoreProvider extends DataStoreProvider { default DataStoreUsageCategory getUsageCategory() { return DataStoreUsageCategory.SHELL; } + + @Override + default ObservableValue informationString(StoreSection section) { + return DataStoreFormatter.shellInformation(section.getWrapper()); + } } diff --git a/openapi.yaml b/openapi.yaml index 1d77e7a5f..1db378cd2 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -627,6 +627,9 @@ components: osName: type: string description: The display name of the operating system + ttyState: + type: string + description: Whether a tty/pty has been allocated for the connection. If allocated, input and output will be unreliable. It is not recommended to use a shell connection then. temp: type: string description: The location of the temporary directory