mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-10-06 15:11:38 +00:00
Rework shell stores, other various small changes
This commit is contained in:
1 parent
02b622fcf9
commit
85389d26f9
40 files changed
+626
-553
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package io.xpipe.core.charsetter;
|
||||
|
||||
import io.xpipe.core.store.FileStore;
|
||||
import io.xpipe.core.store.MachineStore;
|
||||
import io.xpipe.core.store.StreamDataStore;
|
||||
import lombok.Value;
|
||||
|
||||
@@ -107,10 +108,11 @@ public abstract class Charsetter {
|
||||
}
|
||||
}
|
||||
|
||||
if (store instanceof FileStore fileStore) {
|
||||
var newline = fileStore.getMachine().getNewLine();
|
||||
if (store instanceof FileStore fileStore && fileStore.getFileSystem() instanceof MachineStore m) {
|
||||
if (result.getNewLine() == null) {
|
||||
result = new Result(result.getCharset(), newline);
|
||||
try (var pc = m.create().start()) {
|
||||
result = new Result(result.getCharset(), pc.getShellType().getNewLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ import io.xpipe.core.source.RawWriteConnection;
|
||||
import io.xpipe.core.source.WriteMode;
|
||||
import io.xpipe.core.store.StreamDataStore;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
@JsonTypeName("binary")
|
||||
@SuperBuilder
|
||||
@Jacksonized
|
||||
public class BinarySource extends RawDataSource<StreamDataStore> {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -46,8 +46,14 @@ public abstract class DataSource<DS extends DataStore> extends JacksonizedValue
|
||||
throw new AssertionError(ex);
|
||||
}
|
||||
}
|
||||
public void test() throws Exception {
|
||||
store.validate();
|
||||
|
||||
public boolean isComplete() {
|
||||
try {
|
||||
checkComplete();
|
||||
return true;
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void checkComplete() throws Exception {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package io.xpipe.core.store;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public interface CommandProcessControl extends ProcessControl {
|
||||
|
||||
default InputStream startExternalStdout() throws Exception {
|
||||
start();
|
||||
discardErr();
|
||||
return new FilterInputStream(getStdout()) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
getStdout().close();
|
||||
CommandProcessControl.this.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
default OutputStream startExternalStdin() throws Exception {
|
||||
try (CommandProcessControl pc = start()) {
|
||||
pc.discardOut();
|
||||
pc.discardErr();
|
||||
return new FilterOutputStream(getStdin()) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
pc.getStdin().close();
|
||||
pc.close();
|
||||
}
|
||||
};
|
||||
} catch (Exception e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
CommandProcessControl customCharset(Charset charset);
|
||||
|
||||
int getExitCode();
|
||||
|
||||
CommandProcessControl elevated();
|
||||
|
||||
@Override
|
||||
CommandProcessControl start() throws Exception;
|
||||
|
||||
@Override
|
||||
CommandProcessControl exitTimeout(int timeout);
|
||||
|
||||
String readOnlyStdout() throws Exception;
|
||||
|
||||
public default void discardOrThrow() throws Exception {
|
||||
readOrThrow();
|
||||
}
|
||||
|
||||
public default boolean startAndCheckExit() {
|
||||
try (var pc = start()) {
|
||||
return pc.discardAndCheckExit();
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public default boolean discardAndCheckExit() {
|
||||
try {
|
||||
discardOrThrow();
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public default Optional<String> readStderrIfPresent() throws Exception {
|
||||
discardOut();
|
||||
var bytes = getStderr().readAllBytes();
|
||||
var string = new String(bytes, getCharset());
|
||||
var ec = waitFor();
|
||||
return ec ? Optional.of(string) : Optional.empty();
|
||||
}
|
||||
|
||||
public default String readOrThrow() throws Exception {
|
||||
AtomicReference<String> readError = new AtomicReference<>("");
|
||||
var errorThread = new Thread(() -> {
|
||||
try {
|
||||
|
||||
readError.set(new String(getStderr().readAllBytes(), getCharset()));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
errorThread.setDaemon(true);
|
||||
errorThread.start();
|
||||
|
||||
AtomicReference<String> read = new AtomicReference<>("");
|
||||
var t = new Thread(() -> {
|
||||
try {
|
||||
read.set(readLine());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
|
||||
var ec = waitFor();
|
||||
if (!ec) {
|
||||
throw new ProcessOutputException("Command timed out");
|
||||
}
|
||||
|
||||
var exitCode = getExitCode();
|
||||
if (exitCode == 0 && !(read.get().isEmpty() && !readError.get().isEmpty())) {
|
||||
return read.get().trim();
|
||||
} else {
|
||||
throw new ProcessOutputException(
|
||||
"Command returned with " + ec + ": " + readError.get().trim());
|
||||
}
|
||||
}
|
||||
|
||||
Thread discardOut();
|
||||
|
||||
Thread discardErr();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.xpipe.core.store;
|
||||
|
||||
public interface CommandsStore extends DataStore {
|
||||
|
||||
CommandProcessControl create() throws Exception;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import java.io.OutputStream;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Represents a file located on a certain machine.
|
||||
* Represents a file located on a file system.
|
||||
*/
|
||||
@JsonTypeName("file")
|
||||
@SuperBuilder
|
||||
@@ -19,16 +19,16 @@ import java.nio.file.Path;
|
||||
@Getter
|
||||
public class FileStore extends JacksonizedValue implements FilenameStore, StreamDataStore {
|
||||
|
||||
MachineFileStore machine;
|
||||
FileSystemStore fileSystem;
|
||||
String file;
|
||||
|
||||
public FileStore(MachineFileStore machine, String file) {
|
||||
this.machine = machine;
|
||||
public FileStore(FileSystemStore fileSystem, String file) {
|
||||
this.fileSystem = fileSystem;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public final boolean isLocal() {
|
||||
return machine instanceof LocalStore;
|
||||
return fileSystem instanceof LocalStore;
|
||||
}
|
||||
|
||||
public static FileStore local(Path p) {
|
||||
@@ -44,7 +44,7 @@ public class FileStore extends JacksonizedValue implements FilenameStore, Stream
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws Exception {
|
||||
if (machine == null) {
|
||||
if (fileSystem == null) {
|
||||
throw new IllegalStateException("Machine is missing");
|
||||
}
|
||||
if (file == null) {
|
||||
@@ -54,17 +54,17 @@ public class FileStore extends JacksonizedValue implements FilenameStore, Stream
|
||||
|
||||
@Override
|
||||
public InputStream openInput() throws Exception {
|
||||
return machine.openInput(file);
|
||||
return fileSystem.openInput(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream openOutput() throws Exception {
|
||||
return machine.openOutput(file);
|
||||
return fileSystem.openOutput(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canOpen() throws Exception {
|
||||
return machine.exists(file);
|
||||
return fileSystem.exists(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-6
@@ -1,11 +1,9 @@
|
||||
package io.xpipe.core.store;
|
||||
|
||||
import io.xpipe.core.charsetter.NewLine;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface MachineFileStore extends DataStore {
|
||||
public interface FileSystemStore extends DataStore {
|
||||
|
||||
InputStream openInput(String file) throws Exception;
|
||||
|
||||
@@ -13,7 +11,5 @@ public interface MachineFileStore extends DataStore {
|
||||
|
||||
public boolean exists(String file) throws Exception;
|
||||
|
||||
void mkdirs(String file) throws Exception;
|
||||
|
||||
NewLine getNewLine() throws Exception;
|
||||
boolean mkdirs(String file) throws Exception;
|
||||
}
|
||||
@@ -1,21 +1,16 @@
|
||||
package io.xpipe.core.store;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import io.xpipe.core.charsetter.NewLine;
|
||||
import io.xpipe.core.util.JacksonizedValue;
|
||||
import io.xpipe.core.util.SecretValue;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.ServiceLoader;
|
||||
|
||||
@JsonTypeName("local")
|
||||
public class LocalStore extends JacksonizedValue implements MachineFileStore, StandardShellStore {
|
||||
public class LocalStore extends JacksonizedValue implements FileSystemStore, MachineStore {
|
||||
|
||||
@Override
|
||||
public boolean isLocal() {
|
||||
@@ -28,13 +23,13 @@ public class LocalStore extends JacksonizedValue implements MachineFileStore, St
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mkdirs(String file) throws Exception {
|
||||
Files.createDirectories(Path.of(file).getParent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public NewLine getNewLine() {
|
||||
return ShellTypes.getPlatformDefault().getNewLine();
|
||||
public boolean mkdirs(String file) throws Exception {
|
||||
try {
|
||||
Files.createDirectories(Path.of(file).getParent());
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,98 +44,23 @@ public class LocalStore extends JacksonizedValue implements MachineFileStore, St
|
||||
var p = Path.of(file);
|
||||
return Files.newOutputStream(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessControl prepareCommand(List<SecretValue> input, List<String> cmd, Integer timeout, Charset charset) {
|
||||
return new LocalProcessControl(input, cmd, getEffectiveTimeOut(timeout), charset);
|
||||
public ShellProcessControl create() {
|
||||
return LocalProcessControlProvider.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessControl preparePrivilegedCommand(List<SecretValue> input, List<String> cmd, Integer timeOut, Charset charset)
|
||||
throws Exception {
|
||||
return new LocalProcessControl(input, cmd, getEffectiveTimeOut(timeOut), charset);
|
||||
}
|
||||
public static abstract class LocalProcessControlProvider {
|
||||
|
||||
@Override
|
||||
public ShellType determineType() throws Exception {
|
||||
return ShellTypes.getPlatformDefault();
|
||||
}
|
||||
private static LocalProcessControlProvider INSTANCE;
|
||||
|
||||
class LocalProcessControl extends ProcessControl {
|
||||
|
||||
private final List<SecretValue> input;
|
||||
private final Integer timeout;
|
||||
|
||||
@Getter
|
||||
private final List<String> command;
|
||||
private final Charset charset;
|
||||
|
||||
private Process process;
|
||||
|
||||
LocalProcessControl(List<SecretValue> input, List<String> cmd, Integer timeout, Charset charset) {
|
||||
this.input = input;
|
||||
this.timeout = timeout;
|
||||
this.command = cmd;
|
||||
this.charset = charset;
|
||||
public static void init(ModuleLayer layer) {
|
||||
INSTANCE = ServiceLoader.load(layer, LocalProcessControlProvider.class).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private InputStream createInputStream() {
|
||||
var string =
|
||||
input.stream().map(secret -> secret.getSecretValue()).collect(Collectors.joining("\n")) + "\r\n";
|
||||
return new ByteArrayInputStream(string.getBytes(charset));
|
||||
public static ShellProcessControl create() {
|
||||
return INSTANCE.createProcessControl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
var type = LocalStore.this.determineType();
|
||||
var l = type.switchTo(command);
|
||||
var builder = new ProcessBuilder(l);
|
||||
process = builder.start();
|
||||
|
||||
var t = new Thread(() -> {
|
||||
try (var inputStream = createInputStream()) {
|
||||
process.getOutputStream().flush();
|
||||
inputStream.transferTo(process.getOutputStream());
|
||||
process.getOutputStream().close();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
//t.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int waitFor() throws Exception {
|
||||
if (timeout != null) {
|
||||
return process.waitFor(timeout, TimeUnit.SECONDS) ? 0 : -1;
|
||||
} else {
|
||||
return process.waitFor();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getStdout() {
|
||||
return process.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getStdin() {
|
||||
return process.getOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getStderr() {
|
||||
return process.getErrorStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Charset getCharset() {
|
||||
return charset;
|
||||
}
|
||||
|
||||
public Integer getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
public abstract ShellProcessControl createProcessControl();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.xpipe.core.store;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface MachineStore extends FileSystemStore, ShellStore {
|
||||
|
||||
public default boolean isLocal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public default String queryMachineName() throws Exception {
|
||||
try (CommandProcessControl pc = create().commandListFunction(shellProcessControl ->
|
||||
shellProcessControl.getShellType().getOperatingSystemNameCommand())
|
||||
.start()) {
|
||||
return pc.readOrThrow().trim();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public default InputStream openInput(String file) throws Exception {
|
||||
return create().commandListFunction(proc -> proc.getShellType().createFileReadCommand(file))
|
||||
.startExternalStdout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public default OutputStream openOutput(String file) throws Exception {
|
||||
return create().commandListFunction(proc -> proc.getShellType().createFileWriteCommand(file))
|
||||
.startExternalStdin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public default boolean exists(String file) throws Exception {
|
||||
var r = create().commandListFunction(proc -> proc.getShellType().createFileExistsCommand(file))
|
||||
.start()
|
||||
.discardAndCheckExit();
|
||||
return r;
|
||||
}
|
||||
|
||||
@Override
|
||||
public default boolean mkdirs(String file) throws Exception {
|
||||
var r = create().commandListFunction(proc -> proc.getShellType().createMkdirsCommand(file))
|
||||
.start()
|
||||
.discardAndCheckExit();
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -3,129 +3,60 @@ package io.xpipe.core.store;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class ProcessControl {
|
||||
public interface ProcessControl extends AutoCloseable {
|
||||
|
||||
public String executeAndReadStdout() throws Exception {
|
||||
var pc = this;
|
||||
pc.start();
|
||||
pc.discardErr();
|
||||
var bytes = pc.getStdout().readAllBytes();
|
||||
var string = new String(bytes, pc.getCharset());
|
||||
pc.waitFor();
|
||||
return string;
|
||||
}
|
||||
boolean isRunning();
|
||||
|
||||
public void executeOrThrow() throws Exception {
|
||||
var pc = this;
|
||||
pc.start();
|
||||
pc.discardOut();
|
||||
pc.discardErr();
|
||||
pc.waitFor();
|
||||
}
|
||||
ShellType getShellType();
|
||||
|
||||
public boolean executeAndCheckStatus() {
|
||||
try {
|
||||
executeOrThrow();
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
String readResultLine(String input, boolean captureOutput) throws IOException;
|
||||
|
||||
void writeLine(String line) throws IOException;
|
||||
|
||||
void writeLine(String line, boolean captureOutput) throws IOException;
|
||||
|
||||
void typeLine(String line);
|
||||
|
||||
public default String readOutput() throws IOException {
|
||||
var id = UUID.randomUUID();
|
||||
writeLine("echo " + id, false);
|
||||
String lines = "";
|
||||
while (true) {
|
||||
var newLine = readLine();
|
||||
if (newLine.contains(id.toString())) {
|
||||
if (getShellType().echoesInput()) {
|
||||
readLine();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
lines = lines + newLine + "\n";
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
public Optional<String> executeAndReadStderrIfPresent() throws Exception {
|
||||
var pc = this;
|
||||
pc.start();
|
||||
pc.discardOut();
|
||||
var bytes = pc.getStderr().readAllBytes();
|
||||
var string = new String(bytes, pc.getCharset());
|
||||
var ec = pc.waitFor();
|
||||
return ec != 0 ? Optional.of(string) : Optional.empty();
|
||||
}
|
||||
@Override
|
||||
void close() throws IOException;
|
||||
|
||||
public String executeAndReadStdoutOrThrow()
|
||||
throws Exception {
|
||||
var pc = this;
|
||||
pc.start();
|
||||
String readLine() throws IOException;
|
||||
|
||||
AtomicReference<String> readError = new AtomicReference<>("");
|
||||
var errorThread = new Thread(() -> {
|
||||
try {
|
||||
void kill() throws IOException;
|
||||
|
||||
readError.set(new String(pc.getStderr().readAllBytes(), pc.getCharset()));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
errorThread.setDaemon(true);
|
||||
errorThread.start();
|
||||
ProcessControl exitTimeout(int timeout);
|
||||
|
||||
AtomicReference<String> read = new AtomicReference<>("");
|
||||
var t = new Thread(() -> {
|
||||
try {
|
||||
read.set(new String(pc.getStdout().readAllBytes(), pc.getCharset()));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
ProcessControl start() throws Exception;
|
||||
|
||||
var ec = pc.waitFor();
|
||||
if (ec == -1) {
|
||||
throw new ProcessOutputException("Command timed out");
|
||||
}
|
||||
boolean waitFor() throws Exception;
|
||||
|
||||
if (ec == 0 && !(read.get().isEmpty() && !readError.get().isEmpty())) {
|
||||
return read.get().trim();
|
||||
} else {
|
||||
throw new ProcessOutputException(
|
||||
"Command returned with " + ec + ": " + readError.get().trim());
|
||||
}
|
||||
}
|
||||
InputStream getStdout();
|
||||
|
||||
public Thread discardOut() {
|
||||
var t = new Thread(() -> {
|
||||
try {
|
||||
getStdout().transferTo(OutputStream.nullOutputStream());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
return t;
|
||||
}
|
||||
OutputStream getStdin();
|
||||
|
||||
public Thread discardErr() {
|
||||
var t = new Thread(() -> {
|
||||
try {
|
||||
getStderr().transferTo(OutputStream.nullOutputStream());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
return t;
|
||||
}
|
||||
InputStream getStderr();
|
||||
|
||||
public abstract void start() throws Exception;
|
||||
|
||||
public abstract int waitFor() throws Exception;
|
||||
|
||||
public abstract InputStream getStdout();
|
||||
|
||||
public abstract OutputStream getStdin();
|
||||
|
||||
public abstract InputStream getStderr();
|
||||
|
||||
public abstract Charset getCharset();
|
||||
|
||||
public abstract List<String> getCommand();
|
||||
Charset getCharset();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.xpipe.core.store;
|
||||
|
||||
public abstract class ProcessControlProvider {
|
||||
|
||||
public abstract ProcessControl local();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.xpipe.core.store;
|
||||
|
||||
import io.xpipe.core.util.SecretValue;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public interface ShellProcessControl extends ProcessControl {
|
||||
|
||||
ShellProcessControl elevation(SecretValue value);
|
||||
|
||||
SecretValue getElevationPassword();
|
||||
|
||||
default ShellProcessControl shell(@NonNull ShellType type){
|
||||
return shell(type.openCommand());
|
||||
}
|
||||
|
||||
default ShellProcessControl shell(@NonNull List<String> command) {
|
||||
return shell(
|
||||
command.stream().map(s -> s.contains(" ") ? "\"" + s + "\"" : s).collect(Collectors.joining(" ")));
|
||||
}
|
||||
|
||||
default ShellProcessControl shell(@NonNull String command){
|
||||
return shell(processControl -> command);
|
||||
}
|
||||
|
||||
ShellProcessControl shell(@NonNull Function<ProcessControl, String> command);
|
||||
|
||||
void executeCommand(String command) throws IOException;
|
||||
|
||||
default void executeCommand(List<String> command) throws IOException {
|
||||
executeCommand(
|
||||
command.stream().map(s -> s.contains(" ") ? "\"" + s + "\"" : s).collect(Collectors.joining(" ")));
|
||||
}
|
||||
|
||||
@Override
|
||||
ShellProcessControl start() throws Exception;
|
||||
|
||||
default CommandProcessControl commandListFunction(Function<ShellProcessControl, List<String>> command) {
|
||||
return commandFunction(shellProcessControl ->
|
||||
command.apply(shellProcessControl).stream().map(s -> s.contains(" ") ? "\"" + s + "\"" : s).collect(Collectors.joining(" ")));
|
||||
}
|
||||
|
||||
CommandProcessControl commandFunction(Function<ShellProcessControl, String> command);
|
||||
|
||||
CommandProcessControl command(String command);
|
||||
|
||||
default CommandProcessControl command(List<String> command) {
|
||||
return command(
|
||||
command.stream().map(s -> s.contains(" ") ? "\"" + s + "\"" : s).collect(Collectors.joining(" ")));
|
||||
}
|
||||
|
||||
void exit() throws IOException;
|
||||
|
||||
}
|
||||
@@ -1,43 +1,20 @@
|
||||
package io.xpipe.core.store;
|
||||
|
||||
import io.xpipe.core.util.SecretValue;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public interface ShellStore extends DataStore {
|
||||
|
||||
public default Integer getTimeout() {
|
||||
return null;
|
||||
public static MachineStore local() {
|
||||
return new LocalStore();
|
||||
}
|
||||
|
||||
public default List<SecretValue> getInput() {
|
||||
return List.of();
|
||||
}
|
||||
ShellProcessControl create();
|
||||
|
||||
public default Integer getEffectiveTimeOut(Integer timeout) {
|
||||
if (this.getTimeout() == null) {
|
||||
return timeout;
|
||||
public default ShellType determineType() throws Exception {
|
||||
AtomicReference<ShellType> type = new AtomicReference<>();
|
||||
try (var pc = create().start()) {
|
||||
type.set(pc.getShellType());
|
||||
}
|
||||
if (timeout == null) {
|
||||
return getTimeout();
|
||||
}
|
||||
return Math.min(getTimeout(), timeout);
|
||||
}
|
||||
|
||||
public default ProcessControl prepareCommand(List<String> cmd, Integer timeout, Charset charset) throws Exception {
|
||||
return prepareCommand(List.of(), cmd, timeout, charset);
|
||||
}
|
||||
|
||||
public abstract ProcessControl prepareCommand(List<SecretValue> input, List<String> cmd, Integer timeout, Charset charset)
|
||||
throws Exception;
|
||||
|
||||
public default ProcessControl preparePrivilegedCommand(List<String> cmd, Integer timeout, Charset charset) throws Exception {
|
||||
return preparePrivilegedCommand(List.of(), cmd, timeout, charset);
|
||||
}
|
||||
|
||||
public default ProcessControl preparePrivilegedCommand(List<SecretValue> input, List<String> cmd, Integer timeout, Charset charset)
|
||||
throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
return type.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package io.xpipe.core.store;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import io.xpipe.core.charsetter.NewLine;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
property = "type"
|
||||
)
|
||||
public interface ShellType {
|
||||
|
||||
void elevate(ShellProcessControl control, String command, String displayCommand) throws IOException;
|
||||
|
||||
default void init(ProcessControl proc) throws IOException {
|
||||
}
|
||||
|
||||
default String getExitCommand() {
|
||||
return "exit";
|
||||
}
|
||||
|
||||
String getExitCodeVariable();
|
||||
|
||||
default String getConcatenationOperator() {
|
||||
return ";";
|
||||
}
|
||||
|
||||
String getEchoCommand(String s, boolean newLine);
|
||||
|
||||
List<String> openCommand();
|
||||
String switchTo(String cmd);
|
||||
|
||||
List<String> createMkdirsCommand(String dirs);
|
||||
|
||||
List<String> createFileReadCommand(String file);
|
||||
|
||||
List<String> createFileWriteCommand(String file);
|
||||
|
||||
List<String> createFileExistsCommand(String file);
|
||||
|
||||
Charset determineCharset(ProcessControl control) throws Exception;
|
||||
|
||||
NewLine getNewLine();
|
||||
|
||||
String getName();
|
||||
|
||||
String getDisplayName();
|
||||
|
||||
List<String> getOperatingSystemNameCommand();
|
||||
|
||||
boolean echoesInput();
|
||||
}
|
||||
@@ -2,54 +2,57 @@ package io.xpipe.core.store;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import io.xpipe.core.charsetter.NewLine;
|
||||
import io.xpipe.core.util.SecretValue;
|
||||
import lombok.Value;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ShellTypes {
|
||||
|
||||
public static final StandardShellStore.ShellType POWERSHELL = new PowerShell();
|
||||
public static final StandardShellStore.ShellType CMD = new Cmd();
|
||||
public static final StandardShellStore.ShellType SH = new Sh();
|
||||
public static final ShellType POWERSHELL = new PowerShell();
|
||||
public static final ShellType CMD = new Cmd();
|
||||
public static final ShellType SH = new Sh();
|
||||
|
||||
public static StandardShellStore.ShellType determine(ShellStore store) throws Exception {
|
||||
var o = store.prepareCommand(List.of(), List.of("echo", "$0"), null, StandardCharsets.US_ASCII)
|
||||
.executeAndReadStdoutOrThrow()
|
||||
.strip();
|
||||
if (!o.equals("$0")) {
|
||||
return SH;
|
||||
public static ShellType determine(ProcessControl proc) throws Exception {
|
||||
proc.writeLine("echo -NoEnumerate \"a\"", false);
|
||||
String line;
|
||||
while (true) {
|
||||
line = proc.readLine();
|
||||
if (line.equals("-NoEnumerate a")) {
|
||||
return SH;
|
||||
}
|
||||
|
||||
if (line.contains("echo -NoEnumerate \"a\"")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var o = proc.readLine();
|
||||
|
||||
if (o.equals("a")) {
|
||||
return POWERSHELL;
|
||||
} else if (o.equals("-NoEnumerate \"a\"")) {
|
||||
return CMD;
|
||||
} else {
|
||||
o = store.prepareCommand(
|
||||
List.of(),
|
||||
List.of("(dir 2>&1 *`|echo CMD);&<# rem #>echo PowerShell"),
|
||||
null,
|
||||
StandardCharsets.UTF_16LE)
|
||||
.executeAndReadStdoutOrThrow()
|
||||
.trim();
|
||||
if (o.equals("PowerShell")) {
|
||||
return POWERSHELL;
|
||||
return SH;
|
||||
}
|
||||
}
|
||||
|
||||
public static ShellType[] getAvailable(ShellStore store) throws Exception {
|
||||
try (ProcessControl proc = store.create().start()) {
|
||||
var type = determine(proc);
|
||||
if (type == SH) {
|
||||
return getLinuxShells();
|
||||
} else {
|
||||
return CMD;
|
||||
return getWindowsShells();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static StandardShellStore.ShellType[] getAvailable(ShellStore store) throws Exception {
|
||||
var o = store.prepareCommand(List.of(), List.of("echo", "$0"), null, StandardCharsets.US_ASCII)
|
||||
.executeAndReadStdoutOrThrow();
|
||||
if (o.trim().length() > 0 && !o.trim().equals("$0")) {
|
||||
return getLinuxShells();
|
||||
} else {
|
||||
return getWindowsShells();
|
||||
}
|
||||
}
|
||||
|
||||
public static StandardShellStore.ShellType getRecommendedDefault() {
|
||||
public static ShellType getRecommendedDefault() {
|
||||
if (System.getProperty("os.name").startsWith("Windows")) {
|
||||
return POWERSHELL;
|
||||
} else {
|
||||
@@ -57,7 +60,7 @@ public class ShellTypes {
|
||||
}
|
||||
}
|
||||
|
||||
public static StandardShellStore.ShellType getPlatformDefault() {
|
||||
public static ShellType getPlatformDefault() {
|
||||
if (System.getProperty("os.name").startsWith("Windows")) {
|
||||
return CMD;
|
||||
} else {
|
||||
@@ -65,17 +68,51 @@ public class ShellTypes {
|
||||
}
|
||||
}
|
||||
|
||||
public static StandardShellStore.ShellType[] getWindowsShells() {
|
||||
return new StandardShellStore.ShellType[] {CMD, POWERSHELL};
|
||||
public static ShellType[] getWindowsShells() {
|
||||
return new ShellType[] {CMD, POWERSHELL};
|
||||
}
|
||||
|
||||
public static StandardShellStore.ShellType[] getLinuxShells() {
|
||||
return new StandardShellStore.ShellType[] {SH};
|
||||
public static ShellType[] getLinuxShells() {
|
||||
return new ShellType[] {SH};
|
||||
}
|
||||
|
||||
@JsonTypeName("cmd")
|
||||
@Value
|
||||
public static class Cmd implements StandardShellStore.ShellType {
|
||||
public static class Cmd implements ShellType {
|
||||
|
||||
@Override
|
||||
public String getEchoCommand(String s, boolean newLine) {
|
||||
return newLine ? "echo " + s : "echo | set /p dummyName=" + s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConcatenationOperator() {
|
||||
return "&";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void elevate(ShellProcessControl control, String command, String displayCommand) throws IOException {
|
||||
control.executeCommand("net session >NUL 2>NUL");
|
||||
control.executeCommand("echo %errorLevel%");
|
||||
var exitCode = Integer.parseInt(control.readLine());
|
||||
if (exitCode != 0) {
|
||||
throw new IllegalStateException("The command \"" + displayCommand + "\" requires elevation.");
|
||||
}
|
||||
|
||||
control.executeCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ProcessControl proc) throws IOException {
|
||||
proc.readLine();
|
||||
proc.readLine();
|
||||
proc.readLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExitCodeVariable() {
|
||||
return "%errorlevel%";
|
||||
}
|
||||
|
||||
@Override
|
||||
public NewLine getNewLine() {
|
||||
@@ -83,24 +120,18 @@ public class ShellTypes {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> switchTo(List<String> cmd) {
|
||||
var l = new ArrayList<>(cmd);
|
||||
l.add(0, "cmd.exe");
|
||||
l.add(1, "/c");
|
||||
l.add(2, "@chcp");
|
||||
l.add(3, "65001");
|
||||
l.add(4, ">");
|
||||
l.add(5, "nul");
|
||||
l.add(6, "&&");
|
||||
return l;
|
||||
public List<String> openCommand() {
|
||||
return List.of("cmd");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessControl prepareElevatedCommand(
|
||||
ShellStore st, List<SecretValue> in, List<String> cmd, Integer timeout, String pw, Charset charset)
|
||||
throws Exception {
|
||||
var l = List.of("net", "session", ";", "if", "%errorLevel%", "!=", "0");
|
||||
return st.prepareCommand(List.of(), l, timeout, charset);
|
||||
public String switchTo(String cmd) {
|
||||
return "cmd.exe /c " + cmd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> createMkdirsCommand(String dirs) {
|
||||
return List.of("lmkdir", dirs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -119,8 +150,11 @@ public class ShellTypes {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Charset determineCharset(ShellStore store) throws Exception {
|
||||
return StandardCharsets.UTF_8;
|
||||
public Charset determineCharset(ProcessControl control) throws Exception {
|
||||
var output = control.readResultLine("chcp", true);
|
||||
var matcher = Pattern.compile("\\d+").matcher(output);
|
||||
matcher.find();
|
||||
return Charset.forName("ibm" + matcher.group());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,24 +164,63 @@ public class ShellTypes {
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "cmd.exe";
|
||||
return "cmd";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getOperatingSystemNameCommand() {
|
||||
return List.of("Get-ComputerInfo");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean echoesInput() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("powershell")
|
||||
@Value
|
||||
public static class PowerShell implements StandardShellStore.ShellType {
|
||||
public static class PowerShell implements ShellType {
|
||||
|
||||
@Override
|
||||
public List<String> switchTo(List<String> cmd) {
|
||||
var l = new ArrayList<>(cmd);
|
||||
l.add(0, "powershell.exe");
|
||||
return l;
|
||||
public boolean echoesInput() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void elevate(ShellProcessControl control, String command, String displayCommand) throws IOException {
|
||||
control.executeCommand("([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)");
|
||||
var exitCode = Integer.parseInt(control.readLine());
|
||||
if (exitCode != 0) {
|
||||
throw new IllegalStateException("The command \"" + displayCommand + "\" requires elevation.");
|
||||
}
|
||||
|
||||
control.executeCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExitCodeVariable() {
|
||||
return "$LASTEXITCODE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEchoCommand(String s, boolean newLine) {
|
||||
return newLine ? "echo " + s : String.format("Write-Host \"%s\" -NoNewLine", s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> openCommand() {
|
||||
return List.of("powershell", "/nologo");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String switchTo(String cmd) {
|
||||
return "powershell.exe -Command " + cmd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> createMkdirsCommand(String dirs) {
|
||||
return List.of("New-Item", "-Path", "D:\\temp\\Test Folder", "-ItemType", "Directory");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -166,8 +239,11 @@ public class ShellTypes {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Charset determineCharset(ShellStore store) throws Exception {
|
||||
return StandardCharsets.UTF_16LE;
|
||||
public Charset determineCharset(ProcessControl control) throws Exception {
|
||||
var output = control.readResultLine("chcp", true);
|
||||
var matcher = Pattern.compile("\\d+").matcher(output);
|
||||
matcher.find();
|
||||
return Charset.forName("ibm" + matcher.group());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -193,22 +269,41 @@ public class ShellTypes {
|
||||
|
||||
@JsonTypeName("sh")
|
||||
@Value
|
||||
public static class Sh implements StandardShellStore.ShellType {
|
||||
public static class Sh implements ShellType {
|
||||
|
||||
@Override
|
||||
public List<String> switchTo(List<String> cmd) {
|
||||
return cmd;
|
||||
public void elevate(ShellProcessControl control, String command, String displayCommand) throws IOException {
|
||||
if (control.getElevationPassword().getSecretValue() == null) {
|
||||
throw new IllegalStateException("No password for sudo has been set");
|
||||
}
|
||||
|
||||
control.executeCommand("sudo -S " + switchTo(command));
|
||||
control.writeLine(control.getElevationPassword().getSecretValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessControl prepareElevatedCommand(
|
||||
ShellStore st, List<SecretValue> in, List<String> cmd, Integer timeout, String pw, Charset charset)
|
||||
throws Exception {
|
||||
var l = new ArrayList<>(cmd);
|
||||
l.add(0, "sudo");
|
||||
l.add(1, "-S");
|
||||
var pws = new ByteArrayInputStream(pw.getBytes(determineCharset(st)));
|
||||
return st.prepareCommand(List.of(SecretValue.createForSecretValue(pw)), l, timeout, charset);
|
||||
public String getExitCodeVariable() {
|
||||
return "$?";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEchoCommand(String s, boolean newLine) {
|
||||
return newLine ? "echo " + s : "echo -n " + s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> openCommand() {
|
||||
return List.of("sh");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String switchTo(String cmd) {
|
||||
return "sh -c \"" + cmd + "\"";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> createMkdirsCommand(String dirs) {
|
||||
return List.of("mkdir", "-p", dirs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -227,7 +322,7 @@ public class ShellTypes {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Charset determineCharset(ShellStore st) throws Exception {
|
||||
public Charset determineCharset(ProcessControl st) throws Exception {
|
||||
return StandardCharsets.UTF_8;
|
||||
}
|
||||
|
||||
@@ -250,5 +345,10 @@ public class ShellTypes {
|
||||
public List<String> getOperatingSystemNameCommand() {
|
||||
return List.of("uname", "-o");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean echoesInput() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package io.xpipe.core.store;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import io.xpipe.core.charsetter.NewLine;
|
||||
import io.xpipe.core.util.SecretValue;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
|
||||
public interface StandardShellStore extends MachineFileStore, ShellStore {
|
||||
|
||||
public default ProcessControl prepareLocalCommand(List<SecretValue> input, List<String> cmd, Integer timeout)
|
||||
throws Exception {
|
||||
return prepareCommand(input, cmd, timeout, determineType().determineCharset(this));
|
||||
}
|
||||
|
||||
public default boolean isLocal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public default NewLine getNewLine() throws Exception {
|
||||
return determineType().getNewLine();
|
||||
}
|
||||
|
||||
ShellType determineType() throws Exception;
|
||||
|
||||
public default String querySystemName() throws Exception {
|
||||
var result = prepareCommand(
|
||||
List.of(),
|
||||
determineType().getOperatingSystemNameCommand(),
|
||||
getTimeout(),
|
||||
determineType().determineCharset(this))
|
||||
.executeAndReadStdoutOrThrow();
|
||||
return result.strip();
|
||||
}
|
||||
|
||||
@Override
|
||||
public default InputStream openInput(String file) throws Exception {
|
||||
var type = determineType();
|
||||
var cmd = type.createFileReadCommand(file);
|
||||
var p = prepareCommand(List.of(), cmd, null, type.determineCharset(this));
|
||||
p.start();
|
||||
return p.getStdout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public default OutputStream openOutput(String file) throws Exception {
|
||||
return null;
|
||||
// var type = determineType();
|
||||
// var cmd = type.createFileWriteCommand(file);
|
||||
// var p = prepare(cmd).redirectErrorStream(true);
|
||||
// var proc = p.start();
|
||||
// return proc.getOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public default boolean exists(String file) throws Exception {
|
||||
var type = determineType();
|
||||
var cmd = type.createFileExistsCommand(file);
|
||||
var p = prepareCommand(List.of(), cmd, null, type.determineCharset(this));
|
||||
p.start();
|
||||
return p.waitFor() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public default void mkdirs(String file) throws Exception {}
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
public static interface ShellType {
|
||||
|
||||
List<String> switchTo(List<String> cmd);
|
||||
|
||||
default ProcessControl prepareElevatedCommand(
|
||||
ShellStore st, List<SecretValue> in, List<String> cmd, Integer timeout, String pw, Charset charset)
|
||||
throws Exception {
|
||||
return st.prepareCommand(in, cmd, timeout, charset);
|
||||
}
|
||||
|
||||
List<String> createFileReadCommand(String file);
|
||||
|
||||
List<String> createFileWriteCommand(String file);
|
||||
|
||||
List<String> createFileExistsCommand(String file);
|
||||
|
||||
Charset determineCharset(ShellStore store) throws Exception;
|
||||
|
||||
NewLine getNewLine();
|
||||
|
||||
String getName();
|
||||
|
||||
String getDisplayName();
|
||||
|
||||
List<String> getOperatingSystemNameCommand();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ open module io.xpipe.core {
|
||||
|
||||
uses com.fasterxml.jackson.databind.Module;
|
||||
uses io.xpipe.core.source.WriteMode;
|
||||
uses io.xpipe.core.store.LocalStore.LocalProcessControlProvider;
|
||||
|
||||
provides WriteMode with WriteMode.Replace, WriteMode.Append, WriteMode.Prepend;
|
||||
provides com.fasterxml.jackson.databind.Module with
|
||||
|
||||
Reference in new issue
Block a user