This commit is contained in:
crschnick
2025-03-06 23:46:17 +00:00
parent 693b82dab8
commit 321bb2758b
10 changed files with 207 additions and 217 deletions
@@ -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();
}
}
@@ -85,9 +85,8 @@ public abstract class AppShellChecker {
private Optional<FailureResult> 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")) {
@@ -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()
@@ -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<SecretValue> 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);
}
}
}
@@ -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;
}
}
@@ -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);
@@ -14,7 +14,7 @@ import java.util.function.Predicate;
public class ScriptHierarchy {
public static ScriptHierarchy buildEnabledHierarchy(Predicate<DataStoreEntryRef<SimpleScriptStore>> include) {
var all = new HashSet<>(ScriptStore.getEnabledScripts());
var all = new HashSet<>(ScriptStoreSetup.getEnabledScripts());
// Add individual children of groups
// This is not recursive
@@ -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<Enable
protected final String description;
public static ShellControl controlWithDefaultScripts(ShellControl pc) {
return controlWithScripts(pc, getEnabledScripts());
}
public static ShellControl controlWithScripts(
ShellControl pc, List<DataStoreEntryRef<ScriptStore>> 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<String> 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<DataStoreEntryRef<SimpleScriptStore>> 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<SimpleScriptStore> 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<DataStoreEntryRef<ScriptStore>> getEnabledScripts() {
return DataStorage.get().getStoreEntries().stream()
.filter(dataStoreEntry -> dataStoreEntry.getValidity().isUsable()
&& dataStoreEntry.getStore() instanceof ScriptStore scriptStore
&& scriptStore.getState().isEnabled())
.map(DataStoreEntry::<ScriptStore>ref)
.toList();
}
public static List<DataStoreEntryRef<SimpleScriptStore>> flatten(List<DataStoreEntryRef<ScriptStore>> scripts) {
var seen = new LinkedHashSet<DataStoreEntryRef<SimpleScriptStore>>();
scripts.stream()
.filter(scriptStoreDataStoreEntryRef ->
scriptStoreDataStoreEntryRef.get().getValidity().isUsable())
.forEach(scriptStoreDataStoreEntryRef ->
scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(seen));
var dependencies =
new HashMap<DataStoreEntryRef<? extends ScriptStore>, Set<DataStoreEntryRef<SimpleScriptStore>>>();
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<EnabledStoreState> getStateClass() {
return EnabledStoreState.class;
@@ -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<DataStoreEntryRef<ScriptStore>> 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<String> 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<DataStoreEntryRef<SimpleScriptStore>> 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<SimpleScriptStore> 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<DataStoreEntryRef<ScriptStore>> getEnabledScripts() {
return DataStorage.get().getStoreEntries().stream()
.filter(dataStoreEntry -> dataStoreEntry.getValidity().isUsable()
&& dataStoreEntry.getStore() instanceof ScriptStore scriptStore
&& scriptStore.getState().isEnabled())
.map(DataStoreEntry::<ScriptStore>ref)
.toList();
}
public static List<DataStoreEntryRef<SimpleScriptStore>> flatten(List<DataStoreEntryRef<ScriptStore>> scripts) {
var seen = new LinkedHashSet<DataStoreEntryRef<SimpleScriptStore>>();
scripts.stream()
.filter(scriptStoreDataStoreEntryRef ->
scriptStoreDataStoreEntryRef.get().getValidity().isUsable())
.forEach(scriptStoreDataStoreEntryRef ->
scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(seen));
var dependencies =
new HashMap<DataStoreEntryRef<? extends ScriptStore>, Set<DataStoreEntryRef<SimpleScriptStore>>>();
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;
}
}
@@ -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()),