diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java index e6300ae27..2bfb1a61c 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java @@ -21,8 +21,7 @@ public class FsScriptExchangeImpl extends FsScriptExchange { data = new String(in.readAllBytes(), StandardCharsets.UTF_8); } data = shell.getControl().getShellDialect().prepareScriptContent(data); - var file = ScriptHelper.getExecScriptFile(shell.getControl()); - shell.getControl().view().writeScriptFile(file, data); + var file = ScriptHelper.createExecScript(shell.getControl(), data); return Response.builder().path(file).build(); } } diff --git a/app/src/main/java/io/xpipe/app/core/check/AppShellChecker.java b/app/src/main/java/io/xpipe/app/core/check/AppShellChecker.java index b6adaf606..bf4ed1925 100644 --- a/app/src/main/java/io/xpipe/app/core/check/AppShellChecker.java +++ b/app/src/main/java/io/xpipe/app/core/check/AppShellChecker.java @@ -85,9 +85,8 @@ public abstract class AppShellChecker { private Optional selfTestErrorCheck() { try (var sc = LocalShell.getShell().start()) { - var scriptFile = ScriptHelper.getExecScriptFile(sc); - var scriptContent = sc.getShellDialect().prepareScriptContent("echo test"); - sc.view().writeScriptFile(scriptFile, scriptContent); + var scriptContent = "echo test"; + var scriptFile = ScriptHelper.createExecScript(sc, scriptContent); var out = sc.command(sc.getShellDialect().runScriptCommand(sc, scriptFile.toString())) .readStdoutOrThrow(); if (!out.equals("test")) { diff --git a/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java index a933bce65..6f648908e 100644 --- a/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java @@ -1,10 +1,7 @@ package io.xpipe.app.terminal; import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.util.LocalShell; -import io.xpipe.app.util.ScriptHelper; -import io.xpipe.app.util.SshLocalBridge; -import io.xpipe.app.util.WindowsRegistry; +import io.xpipe.app.util.*; import io.xpipe.core.process.CommandBuilder; import java.nio.file.Files; @@ -69,7 +66,7 @@ public class MobaXTermTerminalType extends ExternalTerminalType.WindowsType { .add("" + b.getPort()); // Don't use local shell to build as it uses cygwin var rawCommand = command.buildSimple(); - var script = ScriptHelper.getExecScriptFile(sc, "sh"); + var script = ShellTemp.getLocalTempDataDirectory("mobaxpipe.sh"); Files.writeString(Path.of(script.toString()), "#!/usr/bin/env bash\n" + rawCommand); var fixedFile = script.toString().replaceAll("\\\\", "/").replaceAll("\\s", "\\$0"); sc.command(CommandBuilder.of() diff --git a/app/src/main/java/io/xpipe/app/util/ScriptHelper.java b/app/src/main/java/io/xpipe/app/util/ScriptHelper.java index b7c1e07a3..aa46921aa 100644 --- a/app/src/main/java/io/xpipe/app/util/ScriptHelper.java +++ b/app/src/main/java/io/xpipe/app/util/ScriptHelper.java @@ -14,11 +14,8 @@ import java.util.stream.Collectors; public class ScriptHelper { - public static int getScriptId() { - // A deterministic approach can cause permission problems when two different users execute the same command on a - // system - // Therefore, use a random approach - return new Random().nextInt(Integer.MAX_VALUE); + public static int getScriptHash(String content) { + return Math.abs(content.hashCode()); } @SneakyThrows @@ -73,20 +70,9 @@ public class ScriptHelper { content += nl + t.getPassthroughExitCommand(); } - return createExecScript(t, processControl, FilePath.of(t.initFileName(processControl)), content); - } - - @SneakyThrows - public static FilePath getExecScriptFile(ShellControl processControl) { - return getExecScriptFile( - processControl, processControl.getShellDialect().getScriptFileEnding()); - } - - @SneakyThrows - public static FilePath getExecScriptFile(ShellControl processControl, String fileEnding) { - var fileName = "xpipe-" + getScriptId(); - var temp = processControl.getSystemTemporaryDirectory(); - return temp.join(fileName + "." + fileEnding); + var hash = getScriptHash(content); + var file = t.getInitFileName(processControl, hash); + return createExecScript(t, processControl, file, content); } @SneakyThrows @@ -96,15 +82,14 @@ public class ScriptHelper { @SneakyThrows public static FilePath createExecScript(ShellDialect type, ShellControl processControl, String content) { - var fileName = "xpipe-" + getScriptId(); + var fileName = "xpipe-" + getScriptHash(content); var temp = processControl.getSystemTemporaryDirectory(); var file = temp.join(fileName + "." + type.getScriptFileEnding()); return createExecScript(type, processControl, file, content); } @SneakyThrows - public static FilePath createExecScript( - ShellDialect type, ShellControl processControl, FilePath file, String content) { + public static FilePath createExecScript(ShellDialect type, ShellControl processControl, FilePath file, String content) { content = type.prepareScriptContent(content); TrackEvent.withTrace("Writing exec script") @@ -125,18 +110,21 @@ public class ScriptHelper { type = parent.getOsType().equals(OsType.WINDOWS) ? ShellDialects.CMD : ShellDialects.SH; } - var fileName = "xpipe-" + getScriptId() + "." + type.getScriptFileEnding(); - var temp = parent.getSystemTemporaryDirectory(); - var file = temp.join(fileName); if (type != parent.getShellDialect()) { try (var sub = parent.subShell(type).start()) { var content = sub.getShellDialect().getAskpass().prepareStderrPassthroughContent(sub, requestId, prefix); + var fileName = "xpipe-" + getScriptHash(content) + "." + type.getScriptFileEnding(); + var temp = parent.getSystemTemporaryDirectory(); + var file = temp.join(fileName); return createExecScript(sub.getShellDialect(), sub, file, content); } } else { var content = parent.getShellDialect().getAskpass().prepareStderrPassthroughContent(parent, requestId, prefix); + var fileName = "xpipe-" + getScriptHash(content) + "." + type.getScriptFileEnding(); + var temp = parent.getSystemTemporaryDirectory(); + var file = temp.join(fileName); return createExecScript(parent.getShellDialect(), parent, file, content); } } @@ -160,31 +148,31 @@ public class ScriptHelper { private static FilePath createTerminalPreparedAskpassScript( List pass, ShellControl parent, ShellDialect type) throws Exception { - var fileName = "xpipe-" + getScriptId() + "." + type.getScriptFileEnding(); + var fileName = "xpipe-" + new Random().nextInt(); var temp = parent.getSystemTemporaryDirectory(); - var file = temp.join(fileName); + var fileBase = temp.join(fileName); if (type != parent.getShellDialect()) { try (var sub = parent.subShell(type).start()) { var content = sub.getShellDialect() .getAskpass() .prepareFixedContent( sub, - file.toString(), + fileBase.toString(), pass.stream() .map(secretValue -> secretValue.getSecretValue()) .toList()); - return createExecScript(sub.getShellDialect(), sub, file, content); + return createExecScript(sub.getShellDialect(), sub, content); } } else { var content = parent.getShellDialect() .getAskpass() .prepareFixedContent( parent, - file.toString(), + fileBase.toString(), pass.stream() .map(secretValue -> secretValue.getSecretValue()) .toList()); - return createExecScript(parent.getShellDialect(), parent, file, content); + return createExecScript(parent.getShellDialect(), parent, content); } } } diff --git a/app/src/main/java/io/xpipe/app/util/ShellTemp.java b/app/src/main/java/io/xpipe/app/util/ShellTemp.java index a4dd08cf3..ed633bbe0 100644 --- a/app/src/main/java/io/xpipe/app/util/ShellTemp.java +++ b/app/src/main/java/io/xpipe/app/util/ShellTemp.java @@ -96,14 +96,11 @@ public class ShellTemp { .formatted(d.fileArgument(dir), d.fileArgument(dir), d.fileArgument(dir))); } - public static String getSubDirectory(ShellControl proc, String... sub) throws Exception { + public static FilePath getSubDirectory(ShellControl proc, String... sub) throws Exception { var base = proc.getSystemTemporaryDirectory(); - var arr = Stream.concat(Stream.of(base.toString()), Arrays.stream(sub)).toArray(String[]::new); - var dir = FileNames.join(arr); - + var dir = base.join(sub); // We assume that this directory does not exist yet and therefore don't perform any checks - proc.getShellDialect().prepareUserTempDirectory(proc, dir).execute(); - + proc.getShellDialect().prepareUserTempDirectory(proc, dir.toString()).execute(); return dir; } } 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 b5b048dcd..747c39d0b 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java @@ -42,7 +42,7 @@ public interface ShellDialect { CommandControl prepareUserTempDirectory(ShellControl shellControl, String directory); - String initFileName(ShellControl sc) throws Exception; + FilePath getInitFileName(ShellControl sc, int hash) throws Exception; CommandControl directoryExists(ShellControl shellControl, String directory); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptHierarchy.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptHierarchy.java index 9772d38ff..83af6751d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptHierarchy.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptHierarchy.java @@ -14,7 +14,7 @@ import java.util.function.Predicate; public class ScriptHierarchy { public static ScriptHierarchy buildEnabledHierarchy(Predicate> include) { - var all = new HashSet<>(ScriptStore.getEnabledScripts()); + var all = new HashSet<>(ScriptStoreSetup.getEnabledScripts()); // Add individual children of groups // This is not recursive diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java index 6542f8742..c63521c7e 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java @@ -1,14 +1,7 @@ package io.xpipe.ext.base.script; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; -import io.xpipe.app.util.ShellTemp; import io.xpipe.app.util.Validators; -import io.xpipe.core.process.ShellControl; -import io.xpipe.core.process.ShellDialect; -import io.xpipe.core.process.ShellInitCommand; import io.xpipe.core.store.*; import lombok.*; @@ -30,165 +23,6 @@ public abstract class ScriptStore implements DataStore, StatefulDataStore> enabledScripts) { - try { - // Don't copy scripts if we don't want to modify the file system - if (!pc.getEffectiveSecurityPolicy().permitTempScriptCreation()) { - return pc; - } - - var initFlattened = flatten(enabledScripts).stream() - .filter(store -> store.getStore().isInitScript()) - .toList(); - var bringFlattened = flatten(enabledScripts).stream() - .filter(store -> store.getStore().isShellScript()) - .toList(); - - // Optimize if we have nothing to do - if (initFlattened.isEmpty() && bringFlattened.isEmpty()) { - return pc; - } - - initFlattened.forEach(simpleScriptStore -> { - pc.withInitSnippet(simpleScriptStore.getStore()); - }); - if (!bringFlattened.isEmpty()) { - pc.withInitSnippet(new ShellInitCommand() { - - String dir; - - @Override - public Optional terminalContent(ShellControl shellControl) throws Exception { - if (dir == null) { - dir = initScriptsDirectory(shellControl, bringFlattened); - } - - if (dir == null) { - return Optional.empty(); - } - - return Optional.ofNullable( - shellControl.getShellDialect().addToPathVariableCommand(List.of(dir), true)); - } - - @Override - public boolean canPotentiallyRunInDialect(ShellDialect dialect) { - return true; - } - - @Override - public boolean runInTerminal() { - return true; - } - }); - } - return pc; - } catch (StackOverflowError t) { - throw ErrorEvent.expected( - new RuntimeException("Unable to set up scripts. Is there a circular script dependency?", t)); - } catch (Throwable t) { - throw new RuntimeException("Unable to set up scripts", t); - } - } - - private static String initScriptsDirectory(ShellControl proc, List> refs) - throws Exception { - if (refs.isEmpty()) { - return null; - } - - var applicable = refs.stream() - .filter(simpleScriptStore -> - simpleScriptStore.getStore().getMinimumDialect().isCompatibleTo(proc.getShellDialect())) - .toList(); - if (applicable.isEmpty()) { - return null; - } - - var hash = refs.stream() - .mapToInt(value -> - value.get().getName().hashCode() + value.getStore().hashCode()) - .sum(); - var targetDir = ShellTemp.createUserSpecificTempDataDirectory(proc, "scripts") - .join(proc.getShellDialect().getId()) - .toString(); - var hashFile = FileNames.join(targetDir, "hash"); - var d = proc.getShellDialect(); - if (d.createFileExistsCommand(proc, hashFile).executeAndCheck()) { - var read = d.getFileReadCommand(proc, hashFile).readStdoutOrThrow(); - try { - var readHash = Integer.parseInt(read); - if (hash == readHash) { - return targetDir; - } - } catch (NumberFormatException e) { - ErrorEvent.fromThrowable(e).expected().omit().handle(); - } - } - - if (d.directoryExists(proc, targetDir).executeAndCheck()) { - d.deleteFileOrDirectory(proc, targetDir).execute(); - } - proc.executeSimpleCommand(d.getMkdirsCommand(targetDir)); - - for (DataStoreEntryRef scriptStore : refs) { - var content = d.prepareScriptContent(scriptStore.getStore().getCommands()); - var fileName = proc.getOsType() - .makeFileSystemCompatible( - scriptStore.get().getName().toLowerCase(Locale.ROOT).replaceAll(" ", "_")); - var scriptFile = FileNames.join(targetDir, fileName + "." + d.getScriptFileEnding()); - proc.view().writeScriptFile(FilePath.of(scriptFile), content); - } - - proc.view().writeTextFile(FilePath.of(hashFile), String.valueOf(hash)); - return targetDir; - } - - public static List> getEnabledScripts() { - return DataStorage.get().getStoreEntries().stream() - .filter(dataStoreEntry -> dataStoreEntry.getValidity().isUsable() - && dataStoreEntry.getStore() instanceof ScriptStore scriptStore - && scriptStore.getState().isEnabled()) - .map(DataStoreEntry::ref) - .toList(); - } - - public static List> flatten(List> scripts) { - var seen = new LinkedHashSet>(); - scripts.stream() - .filter(scriptStoreDataStoreEntryRef -> - scriptStoreDataStoreEntryRef.get().getValidity().isUsable()) - .forEach(scriptStoreDataStoreEntryRef -> - scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(seen)); - - var dependencies = - new HashMap, Set>>(); - seen.forEach(ref -> { - var f = new HashSet<>(ref.getStore().queryFlattenedScripts()); - f.remove(ref); - dependencies.put(ref, f); - }); - - var sorted = new ArrayList<>(seen); - sorted.sort((o1, o2) -> { - if (dependencies.get(o1).contains(o2)) { - return 1; - } - - if (dependencies.get(o2).contains(o1)) { - return -1; - } - - return 0; - }); - return sorted; - } - @Override public Class getStateClass() { return EnabledStoreState.class; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreSetup.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreSetup.java new file mode 100644 index 000000000..0239e3ac2 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreSetup.java @@ -0,0 +1,176 @@ +package io.xpipe.ext.base.script; + +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.ShellTemp; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.process.ShellDialect; +import io.xpipe.core.process.ShellInitCommand; +import io.xpipe.core.store.FileNames; +import io.xpipe.core.store.FilePath; + +import java.util.*; + +public class ScriptStoreSetup { + + public static ShellControl controlWithDefaultScripts(ShellControl pc) { + return controlWithScripts(pc, getEnabledScripts()); + } + + public static ShellControl controlWithScripts( + ShellControl pc, List> enabledScripts) { + try { + // Don't copy scripts if we don't want to modify the file system + if (!pc.getEffectiveSecurityPolicy().permitTempScriptCreation()) { + return pc; + } + + var initFlattened = flatten(enabledScripts).stream() + .filter(store -> store.getStore().isInitScript()) + .toList(); + var bringFlattened = flatten(enabledScripts).stream() + .filter(store -> store.getStore().isShellScript()) + .toList(); + + // Optimize if we have nothing to do + if (initFlattened.isEmpty() && bringFlattened.isEmpty()) { + return pc; + } + + initFlattened.forEach(simpleScriptStore -> { + pc.withInitSnippet(simpleScriptStore.getStore()); + }); + if (!bringFlattened.isEmpty()) { + pc.withInitSnippet(new ShellInitCommand() { + + String dir; + + @Override + public Optional terminalContent(ShellControl shellControl) throws Exception { + if (dir == null) { + dir = initScriptsDirectory(shellControl, bringFlattened); + } + + if (dir == null) { + return Optional.empty(); + } + + return Optional.ofNullable( + shellControl.getShellDialect().addToPathVariableCommand(List.of(dir), true)); + } + + @Override + public boolean canPotentiallyRunInDialect(ShellDialect dialect) { + return true; + } + + @Override + public boolean runInTerminal() { + return true; + } + }); + } + return pc; + } catch (StackOverflowError t) { + throw ErrorEvent.expected( + new RuntimeException("Unable to set up scripts. Is there a circular script dependency?", t)); + } catch (Throwable t) { + throw new RuntimeException("Unable to set up scripts", t); + } + } + + private static String initScriptsDirectory(ShellControl proc, List> refs) + throws Exception { + if (refs.isEmpty()) { + return null; + } + + var applicable = refs.stream() + .filter(simpleScriptStore -> + simpleScriptStore.getStore().getMinimumDialect().isCompatibleTo(proc.getShellDialect())) + .toList(); + if (applicable.isEmpty()) { + return null; + } + + var hash = refs.stream() + .mapToInt(value -> + value.get().getName().hashCode() + value.getStore().hashCode()) + .sum(); + var targetDir = ShellTemp.createUserSpecificTempDataDirectory(proc, "scripts") + .join(proc.getShellDialect().getId()) + .toString(); + var hashFile = FileNames.join(targetDir, "hash"); + var d = proc.getShellDialect(); + if (d.createFileExistsCommand(proc, hashFile).executeAndCheck()) { + var read = d.getFileReadCommand(proc, hashFile).readStdoutOrThrow(); + try { + var readHash = Integer.parseInt(read); + if (hash == readHash) { + return targetDir; + } + } catch (NumberFormatException e) { + ErrorEvent.fromThrowable(e).expected().omit().handle(); + } + } + + if (d.directoryExists(proc, targetDir).executeAndCheck()) { + d.deleteFileOrDirectory(proc, targetDir).execute(); + } + proc.executeSimpleCommand(d.getMkdirsCommand(targetDir)); + + for (DataStoreEntryRef scriptStore : refs) { + var content = d.prepareScriptContent(scriptStore.getStore().getCommands()); + var fileName = proc.getOsType() + .makeFileSystemCompatible( + scriptStore.get().getName().toLowerCase(Locale.ROOT).replaceAll(" ", "_")); + var scriptFile = FileNames.join(targetDir, fileName + "." + d.getScriptFileEnding()); + proc.view().writeScriptFile(FilePath.of(scriptFile), content); + } + + proc.view().writeTextFile(FilePath.of(hashFile), String.valueOf(hash)); + return targetDir; + } + + public static List> getEnabledScripts() { + return DataStorage.get().getStoreEntries().stream() + .filter(dataStoreEntry -> dataStoreEntry.getValidity().isUsable() + && dataStoreEntry.getStore() instanceof ScriptStore scriptStore + && scriptStore.getState().isEnabled()) + .map(DataStoreEntry::ref) + .toList(); + } + + public static List> flatten(List> scripts) { + var seen = new LinkedHashSet>(); + scripts.stream() + .filter(scriptStoreDataStoreEntryRef -> + scriptStoreDataStoreEntryRef.get().getValidity().isUsable()) + .forEach(scriptStoreDataStoreEntryRef -> + scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(seen)); + + var dependencies = + new HashMap, Set>>(); + seen.forEach(ref -> { + var f = new HashSet<>(ref.getStore().queryFlattenedScripts()); + f.remove(ref); + dependencies.put(ref, f); + }); + + var sorted = new ArrayList<>(seen); + sorted.sort((o1, o2) -> { + if (dependencies.get(o1).contains(o2)) { + return 1; + } + + if (dependencies.get(o2).contains(o1)) { + return -1; + } + + return 0; + }); + return sorted; + } +} 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 452ea507d..b918e685b 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 @@ -11,8 +11,8 @@ import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.terminal.TerminalLauncher; import io.xpipe.app.util.ShellStoreFormat; -import io.xpipe.ext.base.script.ScriptStore; +import io.xpipe.ext.base.script.ScriptStoreSetup; import javafx.beans.property.BooleanProperty; import javafx.beans.value.ObservableValue; @@ -25,7 +25,7 @@ public interface ShellStoreProvider extends DataStoreProvider { public void execute() throws Exception { var replacement = ProcessControlProvider.get().replace(entry.ref()); ShellStore store = replacement.getStore().asNeeded(); - var control = ScriptStore.controlWithDefaultScripts(store.tempControl()); + var control = ScriptStoreSetup.controlWithDefaultScripts(store.tempControl()); TerminalLauncher.open( replacement.get(), DataStorage.get().getStoreEntryDisplayName(replacement.get()),