mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-05-29 07:20:35 +00:00
Rework
This commit is contained in:
@@ -196,8 +196,8 @@ are not able to resolve and install any dependency packages.
|
||||
|
||||
### RHEL-based distros
|
||||
|
||||
The rpm releases are signed with the GPG key https://xpipe.io/signatures/crschnick.asc.
|
||||
You can import it via `rpm --import https://xpipe.io/signatures/crschnick.asc` to allow your rpm-based package manager to verify the release signature.
|
||||
The rpm releases are signed with the GPG key https://xpipe.io/signatures/0xDD3E0AD0.asc.
|
||||
You can import it via `rpm --import https://xpipe.io/signatures/0xDD3E0AD0.asc` to allow your rpm-based package manager to verify the release signature.
|
||||
|
||||
The following rpm installers are available:
|
||||
|
||||
|
||||
+2
-2
@@ -64,11 +64,11 @@ dependencies {
|
||||
}
|
||||
|
||||
api "com.github.weisj:jsvg:1.7.2"
|
||||
api 'io.xpipe:vernacular:1.16'
|
||||
api 'io.xpipe:vernacular:1.17'
|
||||
api 'org.bouncycastle:bcprov-jdk18on:1.83'
|
||||
api 'info.picocli:picocli:4.7.7'
|
||||
api 'org.apache.commons:commons-lang3:3.20.0'
|
||||
api 'io.sentry:sentry:8.20.0'
|
||||
api 'io.sentry:sentry:8.41.0'
|
||||
api 'commons-io:commons-io:2.21.0'
|
||||
api "com.fasterxml.jackson.core:jackson-databind:2.21.1"
|
||||
api "com.fasterxml.jackson.core:jackson-annotations:2.21"
|
||||
|
||||
@@ -56,7 +56,7 @@ public class ActionConfigComp extends SimpleRegionBuilder {
|
||||
});
|
||||
|
||||
var choice = new StoreListChoiceComp<>(
|
||||
listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory());
|
||||
listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null, null);
|
||||
choice.hide(listProp.emptyProperty());
|
||||
choice.maxHeight(450);
|
||||
return choice;
|
||||
@@ -79,7 +79,8 @@ public class ActionConfigComp extends SimpleRegionBuilder {
|
||||
singleProp,
|
||||
DataStore.class,
|
||||
ref -> true,
|
||||
StoreViewState.get().getAllConnectionsCategory());
|
||||
StoreViewState.get().getAllConnectionsCategory(),
|
||||
null);
|
||||
choice.hide(singleProp.isNull());
|
||||
return choice;
|
||||
}
|
||||
@@ -98,7 +99,7 @@ public class ActionConfigComp extends SimpleRegionBuilder {
|
||||
}
|
||||
});
|
||||
|
||||
var area = new IntegratedTextAreaComp(config, false, "action", new SimpleStringProperty("json"));
|
||||
var area = new IntegratedTextAreaComp(config, false, "action", new SimpleStringProperty("json"), true);
|
||||
area.hide(config.isNull());
|
||||
return area;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class ActionConfirmComp extends SimpleRegionBuilder {
|
||||
}
|
||||
|
||||
var choice = new StoreListChoiceComp<>(
|
||||
listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory());
|
||||
listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null, null);
|
||||
choice.maxHeight(450);
|
||||
choice.setEditable(false);
|
||||
choice.hide(listProp.emptyProperty());
|
||||
|
||||
@@ -38,7 +38,7 @@ public interface QuickConnectProvider extends ActionProvider {
|
||||
|
||||
boolean skipDialogIfPossible();
|
||||
|
||||
default void open(DataStoreEntry e) throws Exception {
|
||||
default void open(DataStoreEntry e) {
|
||||
OpenHubMenuLeafProvider.Action.builder().ref(e.ref()).build().executeSync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,12 @@ import com.sun.net.httpserver.HttpServer;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.*;
|
||||
@@ -44,18 +47,16 @@ public class AppBeaconServer {
|
||||
|
||||
@Getter
|
||||
private String localAuthSecret;
|
||||
private FileChannel localLockFileChannel;
|
||||
private FileLock localLockFileLock;
|
||||
|
||||
private AppBeaconServer(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public static void setupPort() {
|
||||
int port = BeaconConfig.getUsedPort();
|
||||
INSTANCE = new AppBeaconServer(port);
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
try {
|
||||
INSTANCE = new AppBeaconServer(BeaconConfig.getUsedPort());
|
||||
INSTANCE.initAuthSecret();
|
||||
INSTANCE.start();
|
||||
TrackEvent.withInfo("Started http server")
|
||||
@@ -113,20 +114,26 @@ public class AppBeaconServer {
|
||||
var file = BeaconConfig.getLocalBeaconAuthFile();
|
||||
// Create and set temp dir permissions for Linux
|
||||
AppLocalTemp.getLocalTempDataDirectory();
|
||||
|
||||
var id = UUID.randomUUID().toString();
|
||||
Files.writeString(file, id);
|
||||
if (OsType.ofLocal() != OsType.WINDOWS) {
|
||||
Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----"));
|
||||
}
|
||||
localAuthSecret = id;
|
||||
|
||||
var lockFile = BeaconConfig.getLocalBeaconLockFile();
|
||||
localLockFileChannel = new RandomAccessFile(lockFile.toFile(), "rw").getChannel();
|
||||
localLockFileLock = localLockFileChannel.tryLock();
|
||||
}
|
||||
|
||||
private void deleteAuthSecret() {
|
||||
var file = BeaconConfig.getLocalBeaconAuthFile();
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
localLockFileLock.release();
|
||||
localLockFileChannel.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
private void start() throws IOException {
|
||||
@@ -139,8 +146,11 @@ public class AppBeaconServer {
|
||||
});
|
||||
return t;
|
||||
});
|
||||
server = HttpServer.create(
|
||||
new InetSocketAddress(Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}), port), 10);
|
||||
var external = AppPrefs.get().allowExternalApiRequests().get() || Boolean.getBoolean("XPIPE_API_SERVER");
|
||||
var addr = external
|
||||
? Inet4Address.getByAddress(new byte[] {0, 0, 0, 0})
|
||||
: Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01});
|
||||
server = HttpServer.create(new InetSocketAddress(addr, port), 10);
|
||||
BeaconInterface.getAll().forEach(beaconInterface -> {
|
||||
var handler = new BeaconRequestHandler<>(beaconInterface);
|
||||
server.createContext(beaconInterface.getPath(), exchange -> {
|
||||
@@ -173,7 +183,7 @@ public class AppBeaconServer {
|
||||
private boolean handleCorsHeaders(HttpExchange exchange) throws IOException {
|
||||
if (AppPrefs.get().enableHttpApi().get()) {
|
||||
exchange.getResponseHeaders()
|
||||
.add("Origin", "http://localhost:" + AppBeaconServer.get().getPort());
|
||||
.add("Origin", "http://localhost:" + getPort());
|
||||
exchange.getResponseHeaders().add("Vary", "Origin");
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Credentials", "true");
|
||||
|
||||
@@ -142,10 +142,13 @@ public class BeaconRequestHandler<T> implements HttpHandler {
|
||||
try {
|
||||
var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0;
|
||||
if (!emptyResponseClass && response != null) {
|
||||
var redact = AppPrefs.get() == null || !AppPrefs.get().developerMode().getValue() || !AppPrefs.get().developerShowSensitiveCommands().get();
|
||||
var redact = AppPrefs.get() == null
|
||||
|| !AppPrefs.get().developerMode().getValue()
|
||||
|| !AppPrefs.get().developerShowSensitiveCommands().get();
|
||||
var mapper = redact ? JacksonMapper.getRedactedSecretMapper() : JacksonMapper.getUnredactSecretMapper();
|
||||
TrackEvent.trace("Sending response:\n" + response);
|
||||
TrackEvent.trace("Sending raw response:\n" + mapper.valueToTree(response).toPrettyString());
|
||||
TrackEvent.trace(
|
||||
"Sending raw response:\n" + mapper.valueToTree(response).toPrettyString());
|
||||
var bytes = JacksonMapper.getDefault()
|
||||
.valueToTree(response)
|
||||
.toPrettyString()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.secret.SecretManager;
|
||||
@@ -32,6 +34,12 @@ public class AskpassExchangeImpl extends AskpassExchange {
|
||||
var shown = AppLayoutModel.get().getQueueEntries().stream().anyMatch(queueEntry -> msg.getPrompt()
|
||||
.equals(queueEntry.getName().getValue()));
|
||||
if (!shown) {
|
||||
var dialogShown = AppCache.getBoolean("touchDialogShown", false);
|
||||
if (!dialogShown) {
|
||||
AppDialog.information("touchNotice");
|
||||
AppCache.update("touchDialogShown", true);
|
||||
}
|
||||
|
||||
var qe = new AppLayoutModel.QueueEntry(
|
||||
new SimpleStringProperty(msg.getPrompt()),
|
||||
new LabelGraphic.IconGraphic("mdi2f-fingerprint"),
|
||||
@@ -41,7 +49,7 @@ public class AskpassExchangeImpl extends AskpassExchange {
|
||||
() -> {
|
||||
AppLayoutModel.get().getQueueEntries().remove(qe);
|
||||
},
|
||||
Duration.ofSeconds(10));
|
||||
Duration.ofSeconds(15));
|
||||
}
|
||||
return Response.builder().value(InPlaceSecretValue.of("")).build();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import lombok.Value;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Value
|
||||
public class AppMcpServer {
|
||||
@@ -30,8 +29,7 @@ public class AppMcpServer {
|
||||
|
||||
McpSyncServer mcpSyncServer;
|
||||
HttpStreamableServerTransportProvider transportProvider;
|
||||
List<McpServerFeatures.SyncToolSpecification> readOnlyTools;
|
||||
List<McpServerFeatures.SyncToolSpecification> mutationTools;
|
||||
List<McpServerFeatures.SyncToolSpecification> tools;
|
||||
|
||||
public static AppMcpServer get() {
|
||||
return INSTANCE;
|
||||
@@ -47,9 +45,10 @@ public class AppMcpServer {
|
||||
null);
|
||||
|
||||
var prompt = McpSchemaFiles.load("prompt.md");
|
||||
var effectivePrompt = AppPrefs.get().mcpAdditionalContext().getValue() != null ?
|
||||
prompt.replace("__CUSTOM__", AppPrefs.get().mcpAdditionalContext().getValue()) :
|
||||
prompt.replace("__CUSTOM__", "");
|
||||
var effectivePrompt = AppPrefs.get().mcpAdditionalContext().getValue() != null
|
||||
? prompt.replace(
|
||||
"__CUSTOM__", AppPrefs.get().mcpAdditionalContext().getValue())
|
||||
: prompt.replace("__CUSTOM__", "");
|
||||
|
||||
McpSyncServer syncServer = io.modelcontextprotocol.server.McpServer.sync(transportProvider)
|
||||
.serverInfo(AppNames.ofCurrent().getName(), AppProperties.get().getVersion())
|
||||
@@ -61,44 +60,27 @@ public class AppMcpServer {
|
||||
.instructions(effectivePrompt)
|
||||
.build();
|
||||
|
||||
var readOnlyTools = new ArrayList<McpServerFeatures.SyncToolSpecification>();
|
||||
readOnlyTools.add(McpTools.help());
|
||||
readOnlyTools.add(McpTools.listSystems());
|
||||
readOnlyTools.add(McpTools.readFile());
|
||||
readOnlyTools.add(McpTools.listFiles());
|
||||
readOnlyTools.add(McpTools.findFile());
|
||||
readOnlyTools.add(McpTools.getFileInfo());
|
||||
var tools = new ArrayList<McpServerFeatures.SyncToolSpecification>();
|
||||
tools.add(McpTools.help());
|
||||
tools.add(McpTools.listSystems());
|
||||
tools.add(McpTools.readFile());
|
||||
tools.add(McpTools.listFiles());
|
||||
tools.add(McpTools.findFile());
|
||||
tools.add(McpTools.getFileInfo());
|
||||
tools.add(McpTools.openTerminal());
|
||||
tools.add(McpTools.createFile());
|
||||
tools.add(McpTools.writeFile());
|
||||
tools.add(McpTools.createDirectory());
|
||||
tools.add(McpTools.runCommand());
|
||||
tools.add(McpTools.runScript());
|
||||
tools.add(McpTools.toggleState());
|
||||
tools.add(McpTools.callApi());
|
||||
|
||||
var mutationTools = new ArrayList<McpServerFeatures.SyncToolSpecification>();
|
||||
mutationTools.add(McpTools.openTerminal());
|
||||
mutationTools.add(McpTools.createFile());
|
||||
mutationTools.add(McpTools.writeFile());
|
||||
mutationTools.add(McpTools.createDirectory());
|
||||
mutationTools.add(McpTools.runCommand());
|
||||
mutationTools.add(McpTools.runScript());
|
||||
mutationTools.add(McpTools.toggleState());
|
||||
mutationTools.add(McpTools.callApi());
|
||||
|
||||
for (McpServerFeatures.SyncToolSpecification readOnlyTool : readOnlyTools) {
|
||||
for (McpServerFeatures.SyncToolSpecification readOnlyTool : tools) {
|
||||
syncServer.addTool(readOnlyTool);
|
||||
}
|
||||
|
||||
var toolsAdded = new AtomicBoolean();
|
||||
AppPrefs.get().enableMcpMutationTools().subscribe(value -> {
|
||||
for (var mutationTool : mutationTools) {
|
||||
if (value) {
|
||||
syncServer.addTool(mutationTool);
|
||||
} else if (toolsAdded.get()) {
|
||||
syncServer.removeTool(mutationTool.tool().name());
|
||||
}
|
||||
}
|
||||
if (value) {
|
||||
toolsAdded.set(true);
|
||||
}
|
||||
syncServer.notifyToolsListChanged();
|
||||
});
|
||||
|
||||
INSTANCE = new AppMcpServer(syncServer, transportProvider, readOnlyTools, mutationTools);
|
||||
INSTANCE = new AppMcpServer(syncServer, transportProvider, tools);
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
|
||||
@@ -5,7 +5,6 @@ import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStorageQuery;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.core.FilePath;
|
||||
@@ -34,13 +33,19 @@ public interface McpToolHandler
|
||||
} catch (BeaconClientException e) {
|
||||
ErrorEventFactory.fromThrowable(e).expected().omit().handle();
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent(e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName())
|
||||
.addTextContent(
|
||||
e.getMessage() != null
|
||||
? e.getMessage()
|
||||
: e.getClass().getSimpleName())
|
||||
.isError(true)
|
||||
.build();
|
||||
} catch (Throwable e) {
|
||||
ErrorEventFactory.fromThrowable(e).omit().handle();
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent(e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName())
|
||||
.addTextContent(
|
||||
e.getMessage() != null
|
||||
? e.getMessage()
|
||||
: e.getClass().getSimpleName())
|
||||
.isError(true)
|
||||
.build();
|
||||
}
|
||||
@@ -131,7 +136,10 @@ public interface McpToolHandler
|
||||
|
||||
if (found.size() > 1) {
|
||||
throw new BeaconClientException("Multiple connections found: "
|
||||
+ found.stream().map(entry -> DataStorage.get().getStorePath(entry).toString()).toList());
|
||||
+ found.stream()
|
||||
.map(entry ->
|
||||
DataStorage.get().getStorePath(entry).toString())
|
||||
.toList());
|
||||
}
|
||||
|
||||
var e = found.getFirst();
|
||||
|
||||
@@ -8,8 +8,6 @@ import io.xpipe.app.hub.comp.StoreViewState;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.process.ScriptHelper;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.process.TerminalInitScriptConfig;
|
||||
import io.xpipe.app.process.WorkingDirectoryFunction;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStorageQuery;
|
||||
import io.xpipe.app.terminal.TerminalLaunch;
|
||||
@@ -46,36 +44,19 @@ public final class McpTools {
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var ro = AppMcpServer.get().getReadOnlyTools().stream()
|
||||
var tools = AppMcpServer.get().getTools().stream()
|
||||
.filter(syncToolSpecification ->
|
||||
!syncToolSpecification.tool().name().equals("help"))
|
||||
.toList();
|
||||
var mu = AppMcpServer.get().getMutationTools();
|
||||
|
||||
var roList = ro.stream()
|
||||
var toolsList = tools.stream()
|
||||
.map(syncToolSpecification ->
|
||||
"- " + syncToolSpecification.tool().name() + ": "
|
||||
+ syncToolSpecification.tool().description())
|
||||
.collect(Collectors.joining("\n"));
|
||||
var muList = mu.stream()
|
||||
.map(syncToolSpecification ->
|
||||
"- " + syncToolSpecification.tool().name() + ": "
|
||||
+ syncToolSpecification.tool().description())
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
var muEnabled = AppPrefs.get().enableMcpMutationTools().get();
|
||||
var muStatus = muEnabled ? "Right now, the mutation tools are enabled." : "Right now, the mutation tools are disabled. When you enable them in the settings menu, the MCP client might need a reconnect to see the changes.";
|
||||
|
||||
var text = """
|
||||
The XPipe MCP server offers the following read-only tools:
|
||||
The XPipe MCP server offers the following tools:
|
||||
%s
|
||||
These tools will not modify anything on your system and are safe to use.
|
||||
|
||||
You can also enable the following potentially destructive tools in the settings menu:
|
||||
%s
|
||||
These tools can perform write operations and other actions that might be potentially destructive.
|
||||
%s
|
||||
""".formatted(roList, muList, muStatus);
|
||||
""".formatted(toolsList);
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent(text)
|
||||
@@ -102,9 +83,14 @@ public final class McpTools {
|
||||
throw new BeaconClientException("No API endpoint found for path " + path);
|
||||
}
|
||||
|
||||
var httpReq = HttpRequest.newBuilder().uri(URI.create("http://localhost:" + AppBeaconServer.get().getPort() + path))
|
||||
.header("Authorization", "Bearer " + AppPrefs.get().apiKey().get())
|
||||
.POST(HttpRequest.BodyPublishers.ofString(payloadJson.toPrettyString())).build();
|
||||
var httpReq = HttpRequest.newBuilder()
|
||||
.uri(URI.create(
|
||||
"http://localhost:" + AppBeaconServer.get().getPort() + path))
|
||||
.header(
|
||||
"Authorization",
|
||||
"Bearer " + AppPrefs.get().apiKey().get())
|
||||
.POST(HttpRequest.BodyPublishers.ofString(payloadJson.toPrettyString()))
|
||||
.build();
|
||||
var httpRes = HttpHelper.client().send(httpReq, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
var resJson = JacksonMapper.getDefault().readTree(httpRes.body());
|
||||
|
||||
@@ -170,7 +170,7 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
|
||||
var splitPane = new LeftSplitPaneComp(vertical, stack)
|
||||
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
|
||||
.applyStructure(struc -> {
|
||||
struc.getLeft().setMinWidth(250);
|
||||
struc.getLeft().setMinWidth(270);
|
||||
struc.getLeft().setMaxWidth(500);
|
||||
});
|
||||
splitPane.disable(model.getBusy());
|
||||
|
||||
@@ -90,7 +90,7 @@ public class BrowserFullSessionComp extends SimpleRegionBuilder {
|
||||
leftSplit.set(d);
|
||||
});
|
||||
splitPane.applyStructure(struc -> {
|
||||
struc.getLeft().setMinWidth(250);
|
||||
struc.getLeft().setMinWidth(270);
|
||||
struc.getLeft().setMaxWidth(500);
|
||||
struc.get().setPickOnBounds(false);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import io.xpipe.app.action.StoreContextAction;
|
||||
import io.xpipe.app.browser.file.BrowserFileTransferOperation;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.ext.ConnectionFileSystem;
|
||||
import io.xpipe.app.ext.FileSystemStore;
|
||||
import io.xpipe.app.process.ParentSystemAccess;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
@@ -53,12 +52,16 @@ public class TransferFilesActionProvider implements ActionProvider {
|
||||
}
|
||||
|
||||
var sourceFs = operation.getFiles().getFirst().getFileSystem();
|
||||
var sourceAccess = sourceFs.getShell().map(ShellControl::getLocalSystemAccess).orElse(null);
|
||||
var sourceSlowRemote = sourceAccess != null && ParentSystemAccess.isEquivalent(sourceAccess, ParentSystemAccess.none());
|
||||
var sourceAccess =
|
||||
sourceFs.getShell().map(ShellControl::getLocalSystemAccess).orElse(null);
|
||||
var sourceSlowRemote =
|
||||
sourceAccess != null && ParentSystemAccess.isEquivalent(sourceAccess, ParentSystemAccess.none());
|
||||
|
||||
var targetFs = operation.getTarget().getFileSystem();
|
||||
var targetAccess = targetFs.getShell().map(ShellControl::getLocalSystemAccess).orElse(null);
|
||||
var targetSlowRemote = targetAccess != null && ParentSystemAccess.isEquivalent(targetAccess, ParentSystemAccess.none());
|
||||
var targetAccess =
|
||||
targetFs.getShell().map(ShellControl::getLocalSystemAccess).orElse(null);
|
||||
var targetSlowRemote =
|
||||
targetAccess != null && ParentSystemAccess.isEquivalent(targetAccess, ParentSystemAccess.none());
|
||||
|
||||
if (!sourceSlowRemote && !targetSlowRemote) {
|
||||
return true;
|
||||
|
||||
@@ -11,7 +11,6 @@ import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.scene.input.Dragboard;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.Value;
|
||||
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
|
||||
@@ -9,6 +9,7 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.SetChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.Region;
|
||||
@@ -21,6 +22,7 @@ import java.util.function.Predicate;
|
||||
public final class BrowserConnectionListComp extends SimpleRegionBuilder {
|
||||
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
private static final PseudoClass BUSY = PseudoClass.getPseudoClass("busy");
|
||||
private final ObservableValue<DataStoreEntry> selected;
|
||||
private final Predicate<StoreEntryWrapper> applicable;
|
||||
private final BiConsumer<StoreEntryWrapper, BooleanProperty> action;
|
||||
@@ -58,6 +60,11 @@ public final class BrowserConnectionListComp extends SimpleRegionBuilder {
|
||||
&& newValue.equals(s.getWrapper().getEntry()));
|
||||
});
|
||||
});
|
||||
busyEntries.addListener((SetChangeListener<? super StoreSection>) change -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
struc.pseudoClassStateChanged(BUSY, change.getSet().contains(s));
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -35,14 +35,16 @@ public final class BrowserConnectionListFilterComp extends SimpleRegionBuilder {
|
||||
this.category,
|
||||
true,
|
||||
ignored -> true)
|
||||
.hgrow()
|
||||
.maxWidth(10000)
|
||||
.style(Styles.LEFT_PILL)
|
||||
.apply(struc -> {
|
||||
AppFontSizes.base(struc);
|
||||
});
|
||||
var filter = FilterComp.ofStoreFilter(this.filter)
|
||||
.style(Styles.RIGHT_PILL)
|
||||
.minWidth(0)
|
||||
.hgrow()
|
||||
.minWidth(100)
|
||||
.prefWidth(120)
|
||||
.apply(struc -> {
|
||||
AppFontSizes.base(struc);
|
||||
filterTrigger.subscribe(() -> {
|
||||
|
||||
@@ -53,18 +53,6 @@ public final class BrowserFileListComp extends SimpleRegionBuilder {
|
||||
this.fileList = fileList;
|
||||
}
|
||||
|
||||
private static void prepareTableScrollFix(TableView<BrowserEntry> table) {
|
||||
table.lookupAll(".scroll-bar").stream()
|
||||
.filter(node -> node.getPseudoClassStates().contains(PseudoClass.getPseudoClass("horizontal")))
|
||||
.findFirst()
|
||||
.ifPresent(node -> {
|
||||
Region region = (Region) node;
|
||||
region.setMinHeight(0);
|
||||
region.setPrefHeight(0);
|
||||
region.setMaxHeight(0);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
return createTable();
|
||||
@@ -162,11 +150,16 @@ public final class BrowserFileListComp extends SimpleRegionBuilder {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fileList.getFileSystemModel().getFilter().getValue() != null) {
|
||||
return AppI18n.get("emptyFilteredDirectory");
|
||||
}
|
||||
|
||||
return AppI18n.get("emptyDirectory");
|
||||
},
|
||||
AppI18n.activeLanguage(),
|
||||
fileList.getFileSystemModel().getBusy(),
|
||||
fileList.getFileSystemModel().getCurrentPath());
|
||||
fileList.getFileSystemModel().getCurrentPath(),
|
||||
fileList.getFileSystemModel().getFilter());
|
||||
placeholder.textProperty().bind(PlatformThread.sync(placeholderText));
|
||||
table.setPlaceholder(placeholder);
|
||||
AppFontSizes.base(placeholder);
|
||||
@@ -182,7 +175,6 @@ public final class BrowserFileListComp extends SimpleRegionBuilder {
|
||||
table.setFixedCellSize(30.0);
|
||||
|
||||
prepareColumnVisibility(table, filenameCol, mtimeCol, modeCol, ownerCol, sizeCol);
|
||||
prepareTableScrollFix(table);
|
||||
prepareTableSelectionModel(table);
|
||||
prepareTableShortcuts(table);
|
||||
prepareTableEntries(table);
|
||||
|
||||
@@ -299,7 +299,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
return getCurrentDirectory();
|
||||
}
|
||||
|
||||
return FileEntry.ofDirectory(getFileSystem(), fallbackFile.getRawFileEntry().getPath().getParent());
|
||||
return FileEntry.ofDirectory(
|
||||
getFileSystem(), fallbackFile.getRawFileEntry().getPath().getParent());
|
||||
}
|
||||
|
||||
public void cdAsync(FilePath path) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import io.xpipe.app.platform.DerivedObservableList;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -117,13 +116,6 @@ public class BrowserHistoryTabComp extends SimpleRegionBuilder {
|
||||
}
|
||||
|
||||
private BaseRegionBuilder<?, ?> createEmptyDisplay() {
|
||||
var docs = new IntroComp("browserWelcomeDocs", new LabelGraphic.IconGraphic("mdi2b-book-open-variant"));
|
||||
docs.setButtonAction(() -> {
|
||||
DocumentationLink.INTRO.open();
|
||||
});
|
||||
docs.setButtonGraphic(new LabelGraphic.IconGraphic("mdi2w-web"));
|
||||
docs.setButtonDefault(true);
|
||||
|
||||
var open = new IntroComp(
|
||||
"browserWelcomeEmpty",
|
||||
new LabelGraphic.CompGraphic(PrettyImageHelper.ofSpecificFixedSize("welcome/hips.svg", 100, 122)));
|
||||
@@ -133,7 +125,7 @@ public class BrowserHistoryTabComp extends SimpleRegionBuilder {
|
||||
DataStorage.get().local().ref(), null, null, null);
|
||||
});
|
||||
|
||||
var list = new IntroListComp(List.of(docs, open));
|
||||
var list = new IntroListComp(List.of(open));
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
@@ -103,9 +103,7 @@ public class BrowserNavBarComp extends RegionStructureBuilder<HBox, BrowserNavBa
|
||||
pathRegion.prefHeightProperty().bind(stack.heightProperty());
|
||||
|
||||
stack.widthProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> {
|
||||
setAlignment(stack, breadcrumbsRegion);
|
||||
});
|
||||
setAlignment(stack, breadcrumbsRegion);
|
||||
});
|
||||
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
|
||||
@@ -10,10 +10,7 @@ import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
import io.xpipe.app.terminal.TerminalDockBrowserComp;
|
||||
import io.xpipe.app.terminal.TerminalDockView;
|
||||
import io.xpipe.app.terminal.TerminalView;
|
||||
import io.xpipe.app.terminal.WindowsTerminalType;
|
||||
import io.xpipe.app.terminal.*;
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
@@ -76,11 +73,10 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
ThreadHelper.sleep(250);
|
||||
}
|
||||
|
||||
var controllable = session.getTerminal().controllable();
|
||||
if (controllable.isEmpty()) {
|
||||
if (!(session.getTerminal() instanceof TerminalView.ControllableTerminalSession t)) {
|
||||
return;
|
||||
}
|
||||
dockModel.trackTerminal(controllable.get(), true);
|
||||
dockModel.trackTerminal(t, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -96,15 +92,16 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
.filter(shellSession -> shellSession.getTerminal().equals(session.getTerminal()))
|
||||
.count();
|
||||
if (others == 0) {
|
||||
session.getTerminal().controllable().ifPresent(controllableTerminalSession -> {
|
||||
controllableTerminalSession.close();
|
||||
});
|
||||
if (session.getTerminal() instanceof TerminalView.ControllableTerminalSession t) {
|
||||
t.getControllable().close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminalClosed(TerminalView.TerminalSession instance) {
|
||||
dockModel.removeTerminal(instance);
|
||||
refreshShowingState();
|
||||
}
|
||||
};
|
||||
@@ -153,7 +150,6 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
|
||||
GlobalTimer.scheduleUntil(Duration.ofMillis(300), false, () -> {
|
||||
if (viewActive.get()) {
|
||||
dockModel.clearDeadTerminals();
|
||||
dockModel.updateCustomBounds();
|
||||
}
|
||||
return closed;
|
||||
|
||||
@@ -2,9 +2,11 @@ package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.browser.action.impl.TransferFilesActionProvider;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppLocalTemp;
|
||||
import io.xpipe.app.core.AppSystemInfo;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.process.OsFileSystem;
|
||||
@@ -62,6 +64,16 @@ public class BrowserTransferModel {
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
|
||||
transferring.addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue) {
|
||||
var shown = AppCache.getBoolean("downloadDialogShown", false);
|
||||
if (!shown) {
|
||||
AppDialog.information("downloadDialog");
|
||||
AppCache.update("downloadDialogShown", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public List<Item> getCurrentItems() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -46,7 +47,12 @@ public class EditFileMenuProvider implements BrowserMenuLeafProvider {
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.E, KeyCombination.SHORTCUT_DOWN);
|
||||
return switch (OsType.ofLocal()) {
|
||||
case OsType.Linux linux -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHORTCUT_DOWN);
|
||||
case OsType.MacOs macOs ->
|
||||
new KeyCodeCombination(KeyCode.DOWN, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN);
|
||||
case OsType.Windows windows -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHORTCUT_DOWN);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,6 +8,7 @@ import io.xpipe.app.browser.menu.BrowserMenuCategory;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -35,7 +36,11 @@ public class OpenFileDefaultMenuProvider implements BrowserMenuLeafProvider {
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.ENTER);
|
||||
return switch (OsType.ofLocal()) {
|
||||
case OsType.Linux ignored -> new KeyCodeCombination(KeyCode.ENTER);
|
||||
case OsType.MacOs ignored -> new KeyCodeCombination(KeyCode.DOWN, KeyCombination.SHORTCUT_DOWN);
|
||||
case OsType.Windows ignored -> new KeyCodeCombination(KeyCode.ENTER);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,9 +12,6 @@ import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -42,11 +39,6 @@ public class OpenFileWithMenuProvider implements BrowserMenuLeafProvider {
|
||||
return BrowserMenuCategory.OPEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("openFileWith");
|
||||
|
||||
+6
-1
@@ -8,6 +8,7 @@ import io.xpipe.app.browser.menu.BrowserMenuCategory;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -30,7 +31,11 @@ public class OpenNativeFileDetailsMenuProvider implements BrowserMenuLeafProvide
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.ALT_DOWN);
|
||||
return switch (OsType.ofLocal()) {
|
||||
case OsType.Linux ignored -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.ALT_DOWN);
|
||||
case OsType.MacOs ignored -> new KeyCodeCombination(KeyCode.I, KeyCombination.SHORTCUT_DOWN);
|
||||
case OsType.Windows ignored -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.ALT_DOWN);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -44,7 +45,11 @@ public class RenameMenuProvider implements BrowserMenuLeafProvider {
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.R, KeyCombination.SHORTCUT_DOWN);
|
||||
return switch (OsType.ofLocal()) {
|
||||
case OsType.Linux linux -> new KeyCodeCombination(KeyCode.F2);
|
||||
case OsType.MacOs macOs -> new KeyCodeCombination(KeyCode.ENTER);
|
||||
case OsType.Windows windows -> new KeyCodeCombination(KeyCode.F2);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+27
-4
@@ -30,6 +30,7 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
sc.view().isInPath("tar", true);
|
||||
sc.view().isInPath("zip", true);
|
||||
sc.view().isInPath("gzip", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,7 +54,7 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
var ext = List.of("zip", "tar", "tar.gz", "tgz", "rar", "xar");
|
||||
var ext = List.of("zip", "tar", "tar.gz", "tgz", "rar", "xar", "gz", "gzip");
|
||||
if (entries.stream().anyMatch(browserEntry -> ext.stream().anyMatch(s -> browserEntry
|
||||
.getRawFileEntry()
|
||||
.getPath()
|
||||
@@ -89,7 +90,8 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider {
|
||||
protected String getExtension() {
|
||||
return "tar";
|
||||
}
|
||||
});
|
||||
},
|
||||
new GzipActionProvider(false));
|
||||
}
|
||||
|
||||
private abstract static class LeafProvider implements BrowserMenuLeafProvider {
|
||||
@@ -107,7 +109,7 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var name = new SimpleStringProperty(directory ? entries.getFirst().getFileName() : null);
|
||||
var name = new SimpleStringProperty(entries.size() == 1 ? entries.getFirst().getFileName() + "." + getExtension() : null);
|
||||
var modal = ModalOverlay.of(
|
||||
"archiveName",
|
||||
RegionBuilder.of(() -> {
|
||||
@@ -178,7 +180,28 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider {
|
||||
protected String getExtension() {
|
||||
return "tar";
|
||||
}
|
||||
});
|
||||
},
|
||||
new GzipActionProvider(directory));
|
||||
}
|
||||
}
|
||||
|
||||
private class GzipActionProvider extends LeafProvider {
|
||||
|
||||
private GzipActionProvider(boolean directory) {
|
||||
super(directory);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void create(String fileName, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = io.xpipe.app.browser.menu.impl.compress.GzipActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
builder.target(model.getTargetDirectoryPath(entries.getFirst()).join(fileName));
|
||||
builder.build().executeAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getExtension() {
|
||||
return "gz";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.xpipe.app.browser.menu.impl.compress;
|
||||
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.process.ShellDialects;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class GunzipActionProvider implements BrowserActionProvider {
|
||||
|
||||
public static FilePath getTarget(FilePath name) {
|
||||
return FilePath.of(name.toString().replaceAll("\\.gz$", "").replaceAll("\\.gzip$", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "gunzip";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
var b = CommandBuilder.of().add("gunzip", "--keep", "--force");
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
b.addFile(entry.getRawFileEntry().getPath());
|
||||
}
|
||||
sc.command(b).execute();
|
||||
model.refreshSync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package io.xpipe.app.browser.menu.impl.compress;
|
||||
|
||||
import io.xpipe.app.action.AbstractAction;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.browser.icon.BrowserIconFileType;
|
||||
import io.xpipe.app.browser.icon.BrowserIcons;
|
||||
import io.xpipe.app.browser.menu.BrowserApplicationPathMenuProvider;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuCategory;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.process.OsFileSystem;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GunzipUnixMenuProvider implements BrowserMenuLeafProvider, BrowserApplicationPathMenuProvider {
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrowserMenuCategory getCategory() {
|
||||
return BrowserMenuCategory.CUSTOM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var dir = entries.size() > 1
|
||||
? "[...]"
|
||||
: GunzipActionProvider.getTarget(
|
||||
entries.getFirst().getRawFileEntry().getPath())
|
||||
.getFileName();
|
||||
return AppI18n.observable("gunzipDirectory", dir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExecutable() {
|
||||
return "gunzip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
if (!BrowserApplicationPathMenuProvider.super.isApplicable(model, entries)
|
||||
|| !BrowserMenuLeafProvider.super.isApplicable(model, entries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return entries.stream()
|
||||
.allMatch(entry -> {
|
||||
var s = entry.getRawFileEntry().getPath().toString();
|
||||
if (s.endsWith(".tar.gz") || s.endsWith(".tgz") || s.equals("tar.gzip")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return s.endsWith(".gz") || s.endsWith(".gzip");
|
||||
})
|
||||
&& model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = GunzipActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package io.xpipe.app.browser.menu.impl.compress;
|
||||
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class GzipActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "gzip";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
@NonNull
|
||||
private final FilePath target;
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
var b = CommandBuilder.of().add("gzip", "--keep", "--force", "--stdout");
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
b.addFile(entry.getRawFileEntry().getPath());
|
||||
}
|
||||
b.add(">").addFile(target);
|
||||
sc.command(b).execute();
|
||||
model.refreshSync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,8 @@ public class UntarActionProvider implements BrowserActionProvider {
|
||||
.withWorkingDirectory(
|
||||
toDirectory
|
||||
? target
|
||||
: model.getTargetDirectoryPath(getEntries().getFirst()))
|
||||
: model.getTargetDirectoryPath(
|
||||
getEntries().getFirst()))
|
||||
.execute();
|
||||
}
|
||||
model.refreshSync();
|
||||
|
||||
@@ -50,7 +50,8 @@ public class UnzipActionProvider implements BrowserActionProvider {
|
||||
.addFile(getTarget(entry.getRawFileEntry().getPath()));
|
||||
}
|
||||
try (var cc = sc.command(command)
|
||||
.withWorkingDirectory(model.getTargetDirectoryPath(getEntries().getFirst()))
|
||||
.withWorkingDirectory(
|
||||
model.getTargetDirectoryPath(getEntries().getFirst()))
|
||||
.start()) {
|
||||
cc.discardOrThrow();
|
||||
}
|
||||
@@ -73,7 +74,8 @@ public class UnzipActionProvider implements BrowserActionProvider {
|
||||
}
|
||||
command.add("-Path").addFile(entry.getRawFileEntry().getPath());
|
||||
sc.command(command)
|
||||
.withWorkingDirectory(model.getTargetDirectoryPath(getEntries().getFirst()))
|
||||
.withWorkingDirectory(
|
||||
model.getTargetDirectoryPath(getEntries().getFirst()))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,9 @@ public class ContextMenuAugment<S extends Region> implements Consumer<S> {
|
||||
|
||||
var r = struc;
|
||||
r.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
var hidden = hide.get();
|
||||
if (mouseEventCheck != null && mouseEventCheck.test(event)) {
|
||||
if (!hide.get()) {
|
||||
if (!hidden) {
|
||||
var cm = contextMenu.get();
|
||||
if (cm != null) {
|
||||
cm.show(r, event.getScreenX(), event.getScreenY());
|
||||
@@ -67,15 +68,18 @@ public class ContextMenuAugment<S extends Region> implements Consumer<S> {
|
||||
}
|
||||
});
|
||||
r.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (keyEventCheck != null && keyEventCheck.test(event)) {
|
||||
if (!hide.get()) {
|
||||
var cm = contextMenu.get();
|
||||
if (cm != null) {
|
||||
cm.show(r, Side.BOTTOM, 0, 0);
|
||||
currentContextMenu.set(cm);
|
||||
if (keyEventCheck != null) {
|
||||
var hidden = hide.get();
|
||||
if (keyEventCheck.test(event)) {
|
||||
if (!hidden) {
|
||||
var cm = contextMenu.get();
|
||||
if (cm != null) {
|
||||
cm.show(r, Side.BOTTOM, 0, 0);
|
||||
currentContextMenu.set(cm);
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ public class AppLayoutComp extends RegionStructureBuilder<BorderPane, AppLayoutC
|
||||
|
||||
if (new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN).match(event)) {
|
||||
if (TerminalDockHubManager.get().getEnabled().get()) {
|
||||
TerminalDockHubManager.get().toggleDock();
|
||||
TerminalDockHubManager.get().triggerDock();
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
package io.xpipe.app.comp.base;
|
||||
|
||||
import atlantafx.base.controls.ToggleSwitch;
|
||||
import io.xpipe.app.comp.RegionBuilder;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import javafx.beans.binding.Bindings;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.xpipe.app.platform.MenuHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.util.Translatable;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -17,9 +18,11 @@ import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
@@ -47,7 +50,8 @@ public class ChoiceComp<T> extends RegionBuilder<ComboBox<T>> {
|
||||
@Override
|
||||
public ComboBox<T> createSimple() {
|
||||
var cb = MenuHelper.<T>createComboBox();
|
||||
cb.setConverter(new StringConverter<>() {
|
||||
|
||||
Supplier<StringConverter<T>> converter = () -> new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(T object) {
|
||||
if (object == null) {
|
||||
@@ -66,7 +70,21 @@ public class ChoiceComp<T> extends RegionBuilder<ComboBox<T>> {
|
||||
public T fromString(String string) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
cb.setConverter(converter.get());
|
||||
|
||||
// Reset converter on language change to force an update
|
||||
// This does not work properly in older JFX versions, see JDK-8384006
|
||||
var ref = new WeakReference<>(cb);
|
||||
AppI18n.activeLanguage().subscribe((v) -> {
|
||||
var refValue = ref.get();
|
||||
if (refValue != null) {
|
||||
Platform.runLater(() -> {
|
||||
refValue.setConverter(converter.get());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
range.subscribe(c -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
var list = FXCollections.observableArrayList(c.keySet());
|
||||
|
||||
@@ -2,9 +2,11 @@ package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.comp.BaseRegionBuilder;
|
||||
import io.xpipe.app.comp.RegionBuilder;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.platform.MenuHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -16,8 +18,11 @@ import javafx.util.StringConverter;
|
||||
|
||||
import lombok.Setter;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ChoicePaneComp extends RegionBuilder<VBox> {
|
||||
|
||||
@@ -44,7 +49,8 @@ public class ChoicePaneComp extends RegionBuilder<VBox> {
|
||||
}
|
||||
});
|
||||
cb.getSelectionModel().select(selected.getValue());
|
||||
cb.setConverter(new StringConverter<>() {
|
||||
|
||||
Supplier<StringConverter<Entry>> converter = () -> new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Entry object) {
|
||||
if (object == null || object.name() == null) {
|
||||
@@ -58,6 +64,19 @@ public class ChoicePaneComp extends RegionBuilder<VBox> {
|
||||
public Entry fromString(String string) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
cb.setConverter(converter.get());
|
||||
|
||||
// Reset converter on language change to force an update
|
||||
// This does not work properly in older JFX versions, see JDK-8384006
|
||||
var ref = new WeakReference<>(cb);
|
||||
AppI18n.activeLanguage().subscribe((v) -> {
|
||||
var refValue = ref.get();
|
||||
if (refValue != null) {
|
||||
Platform.runLater(() -> {
|
||||
refValue.setConverter(converter.get());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var vbox = new VBox(transformer.apply(cb));
|
||||
@@ -69,13 +88,15 @@ public class ChoicePaneComp extends RegionBuilder<VBox> {
|
||||
});
|
||||
|
||||
cb.prefWidthProperty().bind(vbox.widthProperty());
|
||||
|
||||
var regionMap = new HashMap<Entry, Region>();
|
||||
cb.valueProperty().subscribe(n -> {
|
||||
if (n == null) {
|
||||
if (vbox.getChildren().size() > 1) {
|
||||
vbox.getChildren().remove(1);
|
||||
}
|
||||
} else {
|
||||
var region = n.comp().build();
|
||||
var region = regionMap.computeIfAbsent(n, entry -> entry.comp().build());
|
||||
if (vbox.getChildren().size() == 1) {
|
||||
vbox.getChildren().add(region);
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.xpipe.app.comp.BaseRegionBuilder;
|
||||
import io.xpipe.app.comp.RegionBuilder;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.cred.SshIdentityStrategy;
|
||||
import io.xpipe.app.ext.FileSystemStore;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
@@ -126,9 +127,10 @@ public class ContextualFileReferenceChoiceComp extends RegionBuilder<HBox> {
|
||||
source.toString().substring(0, source.toString().length() - 4))
|
||||
: source;
|
||||
|
||||
var pubSource = Path.of(sourceBase + ".pub");
|
||||
var pubSource = SshIdentityStrategy.getPublicKeyPath(FilePath.of(source))
|
||||
.asLocalPath();
|
||||
if (Files.exists(pubSource)) {
|
||||
var pubTarget = Path.of(target.toString() + ".pub");
|
||||
var pubTarget = sync.getTargetLocation().apply(pubSource);
|
||||
handler.addDataFile(pubSource, pubTarget, sync.getPerUser().get());
|
||||
}
|
||||
|
||||
@@ -216,20 +218,30 @@ public class ContextualFileReferenceChoiceComp extends RegionBuilder<HBox> {
|
||||
filePath.subscribe(s -> PlatformThread.runLaterIfNeeded(() -> {
|
||||
prop.set(s != null ? s.toString() : null);
|
||||
}));
|
||||
prop.addListener((observable, oldValue, newValue) -> {
|
||||
filePath.setValue(newValue != null && !newValue.isBlank() ? FilePath.of(newValue.strip()) : null);
|
||||
});
|
||||
var fileNameComp = new TextFieldComp(prop).apply(struc -> HBox.setHgrow(struc, Priority.ALWAYS));
|
||||
|
||||
if (prompt != null) {
|
||||
fileNameComp.apply(struc -> {
|
||||
fileNameComp.apply(struc -> {
|
||||
if (prompt != null) {
|
||||
prompt.subscribe(filePath -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
struc.setPromptText(filePath != null ? filePath.toString() : null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
prop.addListener((observable, oldValue, newValue) -> {
|
||||
if (!struc.isFocused()) {
|
||||
filePath.setValue(newValue != null && !newValue.isBlank() ? FilePath.of(newValue.strip()) : null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
struc.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue) {
|
||||
var v = prop.getValue();
|
||||
filePath.setValue(v != null && !v.isBlank() ? FilePath.of(v.strip()) : null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return fileNameComp;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.xpipe.app.core.AppOpenArguments;
|
||||
import io.xpipe.app.hub.comp.StoreFilter;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -17,8 +18,6 @@ import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
|
||||
import atlantafx.base.controls.CustomTextField;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
@@ -68,6 +67,14 @@ public class FilterComp extends RegionBuilder<CustomTextField> {
|
||||
filter.focusedProperty()));
|
||||
RegionDescriptor.builder().nameKey("search").build().apply(filter);
|
||||
|
||||
filter.focusedProperty().subscribe(f -> {
|
||||
if (f) {
|
||||
Platform.runLater(() -> {
|
||||
filter.selectAll();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
filter.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (new KeyCodeCombination(KeyCode.ESCAPE).match(event)) {
|
||||
filter.clear();
|
||||
@@ -95,15 +102,6 @@ public class FilterComp extends RegionBuilder<CustomTextField> {
|
||||
filterText.setValue(n != null && n.length() > 0 ? n : null);
|
||||
});
|
||||
|
||||
// Fix caret not being visible on right side when overflowing
|
||||
filter.setSkin(filter.createDefaultSkin());
|
||||
Pane pane = (Pane) filter.getChildrenUnmodifiable().getFirst();
|
||||
var rec = new Rectangle();
|
||||
rec.widthProperty().bind(pane.widthProperty().add(2));
|
||||
rec.heightProperty().bind(pane.heightProperty());
|
||||
rec.setSmooth(false);
|
||||
filter.getChildrenUnmodifiable().getFirst().setClip(rec);
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.ScrollBar;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.control.skin.TextAreaSkin;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
@@ -28,17 +31,23 @@ public class IntegratedTextAreaComp extends RegionStructureBuilder<AnchorPane, I
|
||||
private final boolean lazy;
|
||||
private final String identifier;
|
||||
private final ObservableValue<String> fileType;
|
||||
private final boolean fitHeight;
|
||||
|
||||
public IntegratedTextAreaComp(
|
||||
Property<String> value, boolean lazy, String identifier, ObservableValue<String> fileType) {
|
||||
Property<String> value,
|
||||
boolean lazy,
|
||||
String identifier,
|
||||
ObservableValue<String> fileType,
|
||||
boolean fitHeight) {
|
||||
this.value = value;
|
||||
this.lazy = lazy;
|
||||
this.identifier = identifier;
|
||||
this.fileType = fileType;
|
||||
this.fitHeight = fitHeight;
|
||||
}
|
||||
|
||||
public static IntegratedTextAreaComp script(
|
||||
ObservableValue<DataStoreEntryRef<ShellStore>> host, Property<ShellScript> value) {
|
||||
ObservableValue<DataStoreEntryRef<ShellStore>> host, Property<ShellScript> value, boolean fitHeight) {
|
||||
var type = Bindings.createStringBinding(
|
||||
() -> {
|
||||
return host.getValue() != null
|
||||
@@ -49,10 +58,11 @@ public class IntegratedTextAreaComp extends RegionStructureBuilder<AnchorPane, I
|
||||
: "sh";
|
||||
},
|
||||
host);
|
||||
return script(value, type);
|
||||
return script(value, type, fitHeight);
|
||||
}
|
||||
|
||||
public static IntegratedTextAreaComp script(Property<ShellScript> value, ObservableValue<String> fileType) {
|
||||
public static IntegratedTextAreaComp script(
|
||||
Property<ShellScript> value, ObservableValue<String> fileType, boolean fitHeight) {
|
||||
var string = new SimpleStringProperty();
|
||||
value.subscribe(shellScript -> {
|
||||
string.set(shellScript != null ? shellScript.getValue() : null);
|
||||
@@ -60,7 +70,7 @@ public class IntegratedTextAreaComp extends RegionStructureBuilder<AnchorPane, I
|
||||
string.addListener((observable, oldValue, newValue) -> {
|
||||
value.setValue(newValue != null ? new ShellScript(newValue) : null);
|
||||
});
|
||||
var i = new IntegratedTextAreaComp(string, false, "script", fileType);
|
||||
var i = new IntegratedTextAreaComp(string, false, "script", fileType, fitHeight);
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -110,12 +120,26 @@ public class IntegratedTextAreaComp extends RegionStructureBuilder<AnchorPane, I
|
||||
var copyButton = createOpenButton();
|
||||
var pane = new AnchorPane(textAreaStruc.get(), copyButton);
|
||||
pane.setPickOnBounds(false);
|
||||
AnchorPane.setTopAnchor(copyButton, 4.0);
|
||||
AnchorPane.setRightAnchor(copyButton, 4.0);
|
||||
AnchorPane.setLeftAnchor(textAreaStruc.get(), 0.0);
|
||||
AnchorPane.setRightAnchor(textAreaStruc.get(), 0.0);
|
||||
pane.maxHeightProperty().bind(textAreaStruc.get().heightProperty());
|
||||
return new Structure(pane, textAreaStruc.getTextArea());
|
||||
|
||||
if (fitHeight) {
|
||||
pane.maxHeightProperty().bind(textAreaStruc.get().heightProperty());
|
||||
} else {
|
||||
textAreaStruc.getTextArea().prefHeightProperty().bind(pane.heightProperty());
|
||||
}
|
||||
|
||||
TextArea ta = textAreaStruc.getTextArea();
|
||||
ta.setSkin(new TextAreaSkin(ta));
|
||||
var tas = (ScrollPane) ta.lookup(".scroll-pane");
|
||||
tas.viewportBoundsProperty().subscribe(v -> {
|
||||
var bar = (ScrollBar) tas.lookup(".scroll-bar:vertical");
|
||||
var visible = bar != null && bar.isVisible();
|
||||
AnchorPane.setTopAnchor(copyButton, visible ? 14 : 4.0);
|
||||
AnchorPane.setRightAnchor(copyButton, visible ? 14 : 4.0);
|
||||
});
|
||||
|
||||
return new Structure(pane, ta);
|
||||
}
|
||||
|
||||
@Value
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.xpipe.app.comp.RegionBuilder;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.hub.comp.StoreViewState;
|
||||
import io.xpipe.app.platform.DerivedObservableList;
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Platform;
|
||||
@@ -22,6 +23,7 @@ import javafx.scene.layout.VBox;
|
||||
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
@@ -160,6 +162,13 @@ public class ListBoxViewComp<T> extends RegionBuilder<ScrollPane> {
|
||||
Platform.runLater(() -> {
|
||||
dirty.set(true);
|
||||
});
|
||||
GlobalTimer.delay(
|
||||
() -> {
|
||||
Platform.runLater(() -> {
|
||||
dirty.set(true);
|
||||
});
|
||||
},
|
||||
Duration.ofMillis(50));
|
||||
});
|
||||
shown.addListener((ListChangeListener<? super T>) (change) -> {
|
||||
Platform.runLater(() -> {
|
||||
@@ -285,8 +294,8 @@ public class ListBoxViewComp<T> extends RegionBuilder<ScrollPane> {
|
||||
if (pane.getScene().getHeight() > 200) {
|
||||
var sceneNodeBounds = node.localToScene(node.getBoundsInLocal());
|
||||
// Add some margin to preload
|
||||
if (sceneNodeBounds.getMaxY() < -100
|
||||
|| sceneNodeBounds.getMinY() > pane.getScene().getHeight() + 100) {
|
||||
if (sceneNodeBounds.getMaxY() < -250
|
||||
|| sceneNodeBounds.getMinY() > pane.getScene().getHeight() + 250) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,8 @@ public class MarkdownComp extends RegionBuilder<StackPane> {
|
||||
@SneakyThrows
|
||||
private WebView createWebView() {
|
||||
var wv = new WebView();
|
||||
wv.setMinWidth(100);
|
||||
wv.setMinHeight(100);
|
||||
wv.getEngine().setJavaScriptEnabled(false);
|
||||
wv.setContextMenuEnabled(false);
|
||||
wv.setPageFill(Color.TRANSPARENT);
|
||||
@@ -99,10 +101,9 @@ public class MarkdownComp extends RegionBuilder<StackPane> {
|
||||
AppPrefs.get().theme().subscribe((v) -> {
|
||||
var refVal = ref.get();
|
||||
if (refVal != null && v != null) {
|
||||
var theme = v.isDark()
|
||||
? "misc/github-markdown-dark.css"
|
||||
: "misc/github-markdown-light.css";
|
||||
var url = AppResources.getResourceURL(AppResources.MAIN_MODULE, theme).orElseThrow();
|
||||
var theme = v.isDark() ? "misc/github-markdown-dark.css" : "misc/github-markdown-light.css";
|
||||
var url = AppResources.getResourceURL(AppResources.MAIN_MODULE, theme)
|
||||
.orElseThrow();
|
||||
refVal.getEngine().setUserStyleSheetLocation(url.toString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
@@ -65,11 +66,16 @@ public class ModalOverlayComp extends RegionBuilder<Region> {
|
||||
if (lastShowValue != null
|
||||
&& java.time.Duration.between(lastShowValue, Instant.now())
|
||||
.toMillis()
|
||||
> 500) {
|
||||
> 1000) {
|
||||
mouseHandler.handle(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Timeline createCloseBlockedAnimation() {
|
||||
return new Timeline();
|
||||
}
|
||||
});
|
||||
modal.setInTransitionFactory(
|
||||
OsType.ofLocal() == OsType.LINUX ? null : node -> Animations.fadeIn(node, Duration.millis(150)));
|
||||
@@ -216,6 +222,7 @@ public class ModalOverlayComp extends RegionBuilder<Region> {
|
||||
newValue.getGraphic() != null
|
||||
? newValue.getGraphic()
|
||||
: new LabelGraphic.IconGraphic("mdi2i-information-outline")));
|
||||
l.style("title");
|
||||
l.apply(struc -> {
|
||||
struc.setGraphicTextGap(8);
|
||||
AppFontSizes.xl(struc);
|
||||
@@ -235,7 +242,7 @@ public class ModalOverlayComp extends RegionBuilder<Region> {
|
||||
var node = o instanceof ModalButton mb ? toButton(mb) : ((BaseRegionBuilder<?, ?>) o).build();
|
||||
if (o instanceof ModalButton) {
|
||||
node.widthProperty().addListener((observable, oldValue, n) -> {
|
||||
var d = Math.min(Math.max(n.doubleValue(), 70.0), 200.0);
|
||||
var d = Math.clamp(n.doubleValue(), 70.0, 200.0);
|
||||
if (d > max.get()) {
|
||||
max.set(d);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ public class ModalOverlayStackComp extends SimpleRegionBuilder {
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var current = background;
|
||||
for (var i = 0; i < 5; i++) {
|
||||
for (var i = 0; i < 6; i++) {
|
||||
current = buildModalOverlay(current, i);
|
||||
}
|
||||
return current.build();
|
||||
@@ -35,8 +35,8 @@ public class ModalOverlayStackComp extends SimpleRegionBuilder {
|
||||
modalOverlay.addListener((ListChangeListener<? super ModalOverlay>) c -> {
|
||||
var ex = prop.get();
|
||||
// Don't shift just for an index change
|
||||
if (ex != null && modalOverlay.contains(ex)) {
|
||||
currentIndex.set(modalOverlay.indexOf(ex));
|
||||
if (ex != null && c.getList().contains(ex)) {
|
||||
currentIndex.set(c.getList().indexOf(ex));
|
||||
return;
|
||||
} else {
|
||||
currentIndex.set(index);
|
||||
|
||||
@@ -28,6 +28,7 @@ import atlantafx.base.controls.Spacer;
|
||||
import atlantafx.base.theme.Styles;
|
||||
import lombok.Getter;
|
||||
import org.int4.fx.builders.common.AbstractRegionBuilder;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -116,7 +117,9 @@ public class OptionsComp extends RegionBuilder<VBox> {
|
||||
vbox.accessibleTextProperty().bind(joined);
|
||||
|
||||
if (entry.documentationLink() != null) {
|
||||
var link = new Button("... ?");
|
||||
var fi = new FontIcon("mdi2b-book-open-variant");
|
||||
fi.getStyleClass().add("graphic");
|
||||
var link = new Button("?", fi);
|
||||
link.setMinWidth(Region.USE_PREF_SIZE);
|
||||
link.getStyleClass().add(Styles.BUTTON_OUTLINED);
|
||||
link.getStyleClass().add(Styles.ACCENT);
|
||||
@@ -149,9 +152,13 @@ public class OptionsComp extends RegionBuilder<VBox> {
|
||||
descriptionBox.visibleProperty().bind(compRegion.visibleProperty());
|
||||
descriptionBox.managedProperty().bind(compRegion.managedProperty());
|
||||
} else {
|
||||
vbox.getChildren().add(description);
|
||||
var descriptionBox = new HBox(description);
|
||||
descriptionBox.getStyleClass().add("description-box");
|
||||
vbox.getChildren().add(descriptionBox);
|
||||
vbox.getChildren().add(new Spacer(2, Orientation.VERTICAL));
|
||||
VBox.setMargin(description, new Insets(0, 0, 0, 1));
|
||||
VBox.setMargin(descriptionBox, new Insets(0, 0, 0, 1));
|
||||
descriptionBox.visibleProperty().bind(compRegion.visibleProperty());
|
||||
descriptionBox.managedProperty().bind(compRegion.managedProperty());
|
||||
}
|
||||
|
||||
line.getChildren().add(vbox);
|
||||
|
||||
@@ -15,6 +15,7 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class PrettyImageComp extends SimpleRegionBuilder {
|
||||
@@ -113,8 +114,12 @@ public class PrettyImageComp extends SimpleRegionBuilder {
|
||||
|
||||
value.subscribe(update);
|
||||
if (AppPrefs.get() != null) {
|
||||
var ref = new WeakReference<>(update);
|
||||
AppPrefs.get().theme().addListener((observable, oldValue, newValue) -> {
|
||||
update.accept(value.getValue());
|
||||
var v = ref.get();
|
||||
if (v != null) {
|
||||
v.accept(value.getValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.core.InPlaceSecretValue;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -34,11 +35,11 @@ import java.util.Objects;
|
||||
|
||||
public class SecretFieldComp extends RegionStructureBuilder<InputGroup, SecretFieldComp.Structure> {
|
||||
|
||||
private final Property<InPlaceSecretValue> value;
|
||||
private final ObjectProperty<InPlaceSecretValue> value;
|
||||
private final boolean allowCopy;
|
||||
private final List<BaseRegionBuilder<?, ?>> additionalButtons = new ArrayList<>();
|
||||
|
||||
public SecretFieldComp(Property<InPlaceSecretValue> value, boolean allowCopy) {
|
||||
public SecretFieldComp(ObjectProperty<InPlaceSecretValue> value, boolean allowCopy) {
|
||||
this.value = value;
|
||||
this.allowCopy = allowCopy;
|
||||
}
|
||||
@@ -123,6 +124,7 @@ public class SecretFieldComp extends RegionStructureBuilder<InputGroup, SecretFi
|
||||
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
|
||||
ClipboardHelper.copyPassword(value.getValue(), true);
|
||||
})
|
||||
.disable(value.isNull())
|
||||
.describe(d -> d.nameKey("copy"));
|
||||
|
||||
var list = new ArrayList<BaseRegionBuilder<?, ?>>();
|
||||
|
||||
@@ -58,29 +58,34 @@ public class SideMenuBarComp extends RegionBuilder<VBox> {
|
||||
value.setValue(e);
|
||||
}
|
||||
});
|
||||
b.describe(d -> d.name(e.name()));
|
||||
|
||||
var stack = createStyle(e, b);
|
||||
var stack = createStyle(e, b, false);
|
||||
var shortcut = e.combination();
|
||||
if (shortcut != null) {
|
||||
stack.apply(struc -> struc.getProperties().put("shortcut", shortcut));
|
||||
}
|
||||
b.describe(d -> {
|
||||
d.name(e.name());
|
||||
if (shortcut != null) {
|
||||
d.shortcut(shortcut);
|
||||
}
|
||||
});
|
||||
vbox.getChildren().add(stack.build());
|
||||
}
|
||||
|
||||
{
|
||||
var b = new IconButtonComp("mdi2u-update", () -> {
|
||||
var r = UpdateAvailableDialog.showIfNeeded(false);
|
||||
if (!r) {
|
||||
AppPrefs.get().selectCategory("about");
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var r = UpdateAvailableDialog.showIfNeeded(false);
|
||||
if (!r) {
|
||||
AppPrefs.get().selectCategory("about");
|
||||
UpdateHandler uh = AppDistributionType.get().getUpdateHandler();
|
||||
uh.prepareUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
b.describe(d -> d.nameKey("updateAvailableTooltip"));
|
||||
var stack = createStyle(null, b);
|
||||
var stack = createStyle(null, b, false);
|
||||
var h = AppDistributionType.get().getUpdateHandler();
|
||||
stack.hide(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
@@ -94,7 +99,7 @@ public class SideMenuBarComp extends RegionBuilder<VBox> {
|
||||
if (!AppProperties.get().isStaging()) {
|
||||
var b = new IconButtonComp("mdoal-insights", () -> Hyperlinks.open(Hyperlinks.GITHUB_PTB));
|
||||
b.describe(d -> d.nameKey("ptbAvailableTooltip"));
|
||||
var stack = createStyle(null, b);
|
||||
var stack = createStyle(null, b, false);
|
||||
stack.hide(AppLayoutModel.get().getPtbAvailable().not());
|
||||
vbox.getChildren().add(stack.build());
|
||||
}
|
||||
@@ -125,8 +130,10 @@ public class SideMenuBarComp extends RegionBuilder<VBox> {
|
||||
e.consume();
|
||||
});
|
||||
});
|
||||
var stack = createStyle(null, b);
|
||||
(item.isTop() ? topQueueButtons : bottomQueueButtons).getChildren().add(stack.build());
|
||||
var stack = createStyle(null, b, !item.isTop());
|
||||
(item.isTop() ? topQueueButtons : bottomQueueButtons)
|
||||
.getChildren()
|
||||
.add(stack.build());
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -138,7 +145,7 @@ public class SideMenuBarComp extends RegionBuilder<VBox> {
|
||||
return vbox;
|
||||
}
|
||||
|
||||
private BaseRegionBuilder<?, ?> createStyle(AppLayoutModel.Entry e, IconButtonComp b) {
|
||||
private BaseRegionBuilder<?, ?> createStyle(AppLayoutModel.Entry e, IconButtonComp b, boolean highlight) {
|
||||
var selected = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
b.apply(struc -> {
|
||||
@@ -189,14 +196,14 @@ public class SideMenuBarComp extends RegionBuilder<VBox> {
|
||||
.backgroundProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (value.getValue().equals(e)) {
|
||||
return selectedBorder.get();
|
||||
}
|
||||
|
||||
if (struc.isHover()) {
|
||||
return hoverBorder.get();
|
||||
}
|
||||
|
||||
if (highlight || value.getValue().equals(e)) {
|
||||
return selectedBorder.get();
|
||||
}
|
||||
|
||||
return noneBorder.get();
|
||||
},
|
||||
struc.hoverProperty(),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.comp.RegionBuilder;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FailableSupplier;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class TestButtonComp extends RegionBuilder<Button> {
|
||||
|
||||
private final FailableSupplier<Boolean> run;
|
||||
|
||||
@Override
|
||||
public Button createSimple() {
|
||||
AtomicReference<Region> button = new AtomicReference<>();
|
||||
var testButton = new ButtonComp(AppI18n.observable("test"), new FontIcon("mdi2p-play"), () -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
Platform.runLater(() -> {
|
||||
button.get().getStyleClass().removeAll(Styles.SUCCESS, Styles.DANGER, Styles.ACCENT);
|
||||
button.get().getStyleClass().add(Styles.ACCENT);
|
||||
button.get().setDisable(true);
|
||||
});
|
||||
try {
|
||||
boolean r;
|
||||
try {
|
||||
r = run.get();
|
||||
} finally {
|
||||
Platform.runLater(() -> {
|
||||
button.get().setDisable(false);
|
||||
button.get().getStyleClass().removeAll(Styles.SUCCESS, Styles.DANGER, Styles.ACCENT);
|
||||
});
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
if (r) {
|
||||
button.get().getStyleClass().add(Styles.SUCCESS);
|
||||
} else {
|
||||
button.get().getStyleClass().add(Styles.DANGER);
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
Platform.runLater(() -> {
|
||||
button.get().getStyleClass().add(Styles.DANGER);
|
||||
});
|
||||
ErrorEventFactory.fromThrowable(e).expected().handle();
|
||||
}
|
||||
});
|
||||
});
|
||||
testButton.apply(struc -> button.set(struc));
|
||||
testButton.padding(new Insets(6, 10, 6, 6));
|
||||
return testButton.build();
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,7 @@ import io.xpipe.app.platform.PlatformThread;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.skin.TextFieldSkin;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -73,15 +70,6 @@ public class TextFieldComp extends RegionBuilder<TextField> {
|
||||
}
|
||||
});
|
||||
|
||||
// Fix caret not being visible on right side when overflowing
|
||||
text.setSkin(new TextFieldSkin(text));
|
||||
Pane pane = (Pane) text.getChildrenUnmodifiable().getFirst();
|
||||
var rec = new Rectangle();
|
||||
rec.widthProperty().bind(pane.widthProperty().add(2));
|
||||
rec.heightProperty().bind(pane.heightProperty());
|
||||
rec.setSmooth(false);
|
||||
text.getChildrenUnmodifiable().getFirst().setClip(rec);
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
package io.xpipe.app.core;
|
||||
|
||||
import io.xpipe.app.comp.base.ModalButton;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.comp.base.TextAreaComp;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.process.OsFileSystem;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.app.util.TlsCertificateFormat;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.Value;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.*;
|
||||
import java.util.*;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
public class AppCertStore {
|
||||
|
||||
@Value
|
||||
static class Entry {
|
||||
|
||||
String name;
|
||||
Path file;
|
||||
X509Certificate certificate;
|
||||
}
|
||||
|
||||
private class SavingTrustManager implements X509TrustManager {
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return trustManager.getAcceptedIssuers();
|
||||
}
|
||||
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
trustManager.checkClientTrusted(chain, authType);
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
try {
|
||||
trustManager.checkServerTrusted(chain, authType);
|
||||
} catch (CertificateException e) {
|
||||
var cause = e.getCause();
|
||||
var nonTrusted = cause != null
|
||||
&& cause.getClass()
|
||||
.getName()
|
||||
.equals("sun.security.provider.certpath.SunCertPathBuilderException");
|
||||
if (nonTrusted) {
|
||||
showTrustDialog(chain[chain.length - 1]);
|
||||
ErrorEventFactory.preconfigure(
|
||||
ErrorEventFactory.fromThrowable(e).expected().omit());
|
||||
throw e;
|
||||
} else {
|
||||
throw ErrorEventFactory.expected(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
private final List<Entry> certificates;
|
||||
|
||||
private X509TrustManager trustManager;
|
||||
private boolean modalShowing;
|
||||
private final SavingTrustManager savingTrustManager = new SavingTrustManager();
|
||||
|
||||
private AppCertStore(List<Entry> certificates) {
|
||||
this.certificates = certificates;
|
||||
}
|
||||
|
||||
public static Path getDir() {
|
||||
return AppProperties.get().getDataDir().resolve("cacerts");
|
||||
}
|
||||
|
||||
public static Path getBundleFileFilePath() {
|
||||
return getDir().resolve("bundle.pem");
|
||||
}
|
||||
|
||||
public static Optional<Path> getBundleFile() {
|
||||
var file = getBundleFileFilePath();
|
||||
return Files.exists(file) ? Optional.of(file) : Optional.empty();
|
||||
}
|
||||
|
||||
public synchronized void addCertificate(String name, X509Certificate certificate) {
|
||||
if (certificates.stream().anyMatch(entry -> entry.certificate.equals(certificate))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var dir = getDir();
|
||||
Files.createDirectories(dir);
|
||||
var compatName = OsFileSystem.ofLocal().makeFileSystemCompatible(name);
|
||||
var pemFile = dir.resolve(name + ".pem");
|
||||
var pem = convertToPem(certificate);
|
||||
Files.writeString(pemFile, pem);
|
||||
|
||||
var cerFile = dir.resolve(name + ".cer");
|
||||
var cer = certificate.getEncoded();
|
||||
Files.write(cerFile, cer);
|
||||
|
||||
var entry = new Entry(compatName, pemFile, certificate);
|
||||
certificates.add(entry);
|
||||
refreshCertBundle(true);
|
||||
updateTrustManager();
|
||||
} catch (Exception e) {
|
||||
ErrorEventFactory.fromThrowable(e).handle();
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshCertBundle(boolean force) throws Exception {
|
||||
var file = getBundleFileFilePath();
|
||||
if (certificates.isEmpty()) {
|
||||
Files.deleteIfExists(file);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && Files.exists(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var s = new StringBuilder();
|
||||
|
||||
var ks = KeyStore.getInstance("JKS");
|
||||
var caCertsFile = Path.of(System.getProperty("java.home") + "/lib/security/cacerts");
|
||||
try (FileInputStream fis = new FileInputStream(caCertsFile.toFile())) {
|
||||
ks.load(fis, null);
|
||||
}
|
||||
|
||||
Enumeration<String> list = ks.aliases();
|
||||
while (list.hasMoreElements()) {
|
||||
String alias = list.nextElement();
|
||||
// Check if this cert is labeled a trust anchor.
|
||||
if (alias.contains(" [jdk")) {
|
||||
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
|
||||
s.append(convertToPem(cert));
|
||||
}
|
||||
}
|
||||
|
||||
for (Entry e : certificates) {
|
||||
s.append(convertToPem(e.certificate));
|
||||
}
|
||||
Files.writeString(file, s);
|
||||
}
|
||||
|
||||
public X509TrustManager getCustomTrustManager() {
|
||||
return savingTrustManager;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void updateTrustManager() {
|
||||
KeyStore ks = KeyStore.getInstance("JKS");
|
||||
|
||||
var caCertsFile = Path.of(System.getProperty("java.home") + "/lib/security/cacerts");
|
||||
try (FileInputStream fis = new FileInputStream(caCertsFile.toFile())) {
|
||||
ks.load(fis, null);
|
||||
}
|
||||
|
||||
// For testing TLS cert acceptance dialogs
|
||||
// without setting up a custom proxy
|
||||
// var e = ks.aliases();
|
||||
// var list = new ArrayList<String>();
|
||||
// while (e.hasMoreElements()) {
|
||||
// String alias = e.nextElement();
|
||||
// list.add(alias);
|
||||
// }
|
||||
// for (String s : list) {
|
||||
// ks.deleteEntry(s);
|
||||
// }
|
||||
|
||||
for (Entry certificate : certificates) {
|
||||
ks.setCertificateEntry(certificate.getName(), certificate.getCertificate());
|
||||
}
|
||||
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(ks);
|
||||
trustManager = (X509TrustManager) tmf.getTrustManagers()[0];
|
||||
}
|
||||
|
||||
private synchronized void showTrustDialog(X509Certificate certificate) {
|
||||
if (modalShowing) {
|
||||
return;
|
||||
}
|
||||
|
||||
modalShowing = true;
|
||||
ThreadHelper.runAsync(() -> {
|
||||
var format = TlsCertificateFormat.format(certificate);
|
||||
var content = new TextAreaComp(new SimpleStringProperty(format))
|
||||
.applyStructure(structure -> {
|
||||
structure.getTextArea().setEditable(false);
|
||||
})
|
||||
.prefHeight(450);
|
||||
var name = new SimpleStringProperty();
|
||||
var options = new OptionsBuilder()
|
||||
.nameAndDescription("certificateDetails")
|
||||
.addComp(content)
|
||||
.nameAndDescription("certificateName")
|
||||
.addString(name)
|
||||
.nonNull()
|
||||
.buildComp()
|
||||
.prefWidth(650);
|
||||
var modal = ModalOverlay.of("untrustedCertificateTitle", options);
|
||||
modal.addButton(ModalButton.cancel());
|
||||
modal.addButton(new ModalButton(
|
||||
"trust",
|
||||
() -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
addCertificate(name.getValue(), certificate);
|
||||
});
|
||||
},
|
||||
true,
|
||||
true)
|
||||
.augment(button -> {
|
||||
button.disableProperty().bind(name.isNull());
|
||||
}));
|
||||
modal.showAndWait();
|
||||
modalShowing = false;
|
||||
});
|
||||
}
|
||||
|
||||
private static AppCertStore INSTANCE;
|
||||
|
||||
public static AppCertStore get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
var dir = getDir();
|
||||
if (!Files.exists(dir)) {
|
||||
INSTANCE = new AppCertStore(new ArrayList<>());
|
||||
INSTANCE.updateTrustManager();
|
||||
return;
|
||||
}
|
||||
|
||||
var list = new ArrayList<Entry>();
|
||||
try (var stream = Files.list(dir)) {
|
||||
var files = stream.toList();
|
||||
for (Path f : files) {
|
||||
if (f.equals(getBundleFileFilePath())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!f.getFileName().toString().endsWith(".pem")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var cert = parseCertificate(f);
|
||||
var name = FilenameUtils.getBaseName(f.getFileName().toString());
|
||||
list.add(new Entry(name, f, cert));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ErrorEventFactory.fromThrowable(e).expected().handle();
|
||||
}
|
||||
|
||||
INSTANCE = new AppCertStore(list);
|
||||
try {
|
||||
INSTANCE.refreshCertBundle(!AppProperties.get().isDevelopmentEnvironment()
|
||||
&& AppProperties.get().isNewBuildSession());
|
||||
} catch (Exception e) {
|
||||
ErrorEventFactory.fromThrowable(e).expected().handle();
|
||||
}
|
||||
INSTANCE.updateTrustManager();
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
INSTANCE = null;
|
||||
}
|
||||
|
||||
private static X509Certificate parseCertificate(Path file) throws Exception {
|
||||
var b = Files.readAllBytes(file);
|
||||
return (X509Certificate)
|
||||
CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(b));
|
||||
}
|
||||
|
||||
private static String convertToPem(X509Certificate cert) throws CertificateEncodingException {
|
||||
String begin = "-----BEGIN CERTIFICATE-----\n";
|
||||
String end = "\n-----END CERTIFICATE-----\n";
|
||||
byte[] derCert = cert.getEncoded();
|
||||
Base64.Encoder encoder = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8));
|
||||
String pemCertPre = encoder.encodeToString(derCert);
|
||||
String pemCert = begin + pemCertPre + end;
|
||||
return pemCert;
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.PlatformState;
|
||||
import io.xpipe.app.prefs.*;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
@@ -32,12 +32,12 @@ public class AppConfigurationDialog {
|
||||
.sub(PersonalizationCategory.themeChoice())
|
||||
.sub(TerminalCategory.terminalChoice(false));
|
||||
|
||||
if (OsType.ofLocal() != OsType.WINDOWS && AppPrefs.get().terminalMultiplexer().getValue() != null) {
|
||||
if (OsType.ofLocal() != OsType.WINDOWS
|
||||
&& AppPrefs.get().terminalMultiplexer().getValue() != null) {
|
||||
options.sub(TerminalCategory.terminalMultiplexerChoice());
|
||||
}
|
||||
|
||||
var optionsComp = options
|
||||
.sub(EditorCategory.editorChoice())
|
||||
var optionsComp = options.sub(EditorCategory.editorChoice())
|
||||
.sub(PasswordManagerCategory.passwordManagerChoice())
|
||||
.buildComp();
|
||||
optionsComp.style("initial-setup");
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.PlatformState;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import java.awt.*;
|
||||
@@ -51,16 +52,6 @@ public class AppDesktopIntegration {
|
||||
// This will initialize the toolkit on macOS and create the dock icon
|
||||
// macOS does not like applications that run fully in the background, so always do it
|
||||
if (OsType.ofLocal() == OsType.MACOS && Desktop.isDesktopSupported()) {
|
||||
Desktop.getDesktop().setPreferencesHandler(e -> {
|
||||
if (PlatformState.getCurrent() != PlatformState.RUNNING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppLayoutModel.get() != null) {
|
||||
AppLayoutModel.get().selectSettings();
|
||||
}
|
||||
});
|
||||
|
||||
// URL open operations have to be handled in a special way on macOS!
|
||||
Desktop.getDesktop().setOpenURIHandler(e -> {
|
||||
AppOpenArguments.handle(List.of(e.getURI().toString()));
|
||||
@@ -91,9 +82,44 @@ public class AppDesktopIntegration {
|
||||
.handle();
|
||||
}
|
||||
}
|
||||
|
||||
Desktop.getDesktop().setQuitHandler((e, response) -> {
|
||||
response.cancelQuit();
|
||||
ThreadHelper.runAsync(() -> {
|
||||
AppOperationMode.externalShutdown();
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
ErrorEventFactory.fromThrowable(ex).term().handle();
|
||||
}
|
||||
}
|
||||
|
||||
public static void initMenuBar() {
|
||||
if (OsType.ofLocal() == OsType.MACOS && Desktop.isDesktopSupported()) {
|
||||
|
||||
// TODO: These don't show up any more
|
||||
// JavaFX broke them
|
||||
|
||||
Desktop.getDesktop().setPreferencesHandler(e -> {
|
||||
if (PlatformState.getCurrent() != PlatformState.RUNNING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppLayoutModel.get() != null) {
|
||||
AppLayoutModel.get().selectSettings();
|
||||
}
|
||||
});
|
||||
|
||||
Desktop.getDesktop().setAboutHandler(e -> {
|
||||
if (PlatformState.getCurrent() != PlatformState.RUNNING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppLayoutModel.get() != null) {
|
||||
AppLayoutModel.get().selectSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class AppDisplayScale {
|
||||
if (AppPrefs.get() != null) {
|
||||
var s = AppPrefs.get().uiScale().getValue();
|
||||
if (s != null) {
|
||||
var i = Math.min(300, Math.max(25, s));
|
||||
var i = Math.clamp(s, 25, 300);
|
||||
var percent = i / 100.0;
|
||||
return percent;
|
||||
}
|
||||
|
||||
@@ -4,15 +4,20 @@ import io.xpipe.app.beacon.AppBeaconServer;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.beacon.BeaconClient;
|
||||
import io.xpipe.beacon.BeaconClientInformation;
|
||||
import io.xpipe.beacon.BeaconConfig;
|
||||
import io.xpipe.beacon.BeaconServer;
|
||||
import io.xpipe.beacon.api.DaemonFocusExchange;
|
||||
import io.xpipe.beacon.api.DaemonOpenExchange;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -33,8 +38,42 @@ public class AppInstance {
|
||||
}
|
||||
|
||||
private static void checkStart(int attemptCounter) {
|
||||
var port = AppBeaconServer.get().getPort();
|
||||
var port = BeaconConfig.getUsedPort();
|
||||
var reachable = BeaconServer.isReachable(port);
|
||||
|
||||
if (reachable) {
|
||||
// If an instance is running as another user, we cannot connect to it as the xpipe_auth file is inaccessible
|
||||
var authFile = BeaconConfig.getLocalBeaconAuthFile();
|
||||
var hasAuthFile = Files.exists(authFile);
|
||||
|
||||
// Make sure that it is not a leftover
|
||||
if (hasAuthFile) {
|
||||
try (var channel = new RandomAccessFile(BeaconConfig.getLocalBeaconLockFile().toFile(), "rw").getChannel()) {
|
||||
var lock = channel.tryLock();
|
||||
if (lock != null) {
|
||||
lock.release();
|
||||
Files.delete(authFile);
|
||||
hasAuthFile = false;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
if (!hasAuthFile) {
|
||||
var replacement = BeaconConfig.fallBackToAnotherPort();
|
||||
if (replacement.isEmpty()) {
|
||||
ErrorEventFactory.fromMessage("Unable to find free beacon port")
|
||||
.term()
|
||||
.documentationLink(DocumentationLink.BEACON_PORT_BIND)
|
||||
.expected()
|
||||
.handle();
|
||||
AppOperationMode.halt(1);
|
||||
} else {
|
||||
port = replacement.getAsInt();
|
||||
reachable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!reachable) {
|
||||
// Even in case we are unable to reach another beacon server
|
||||
// there might be another instance running, for example
|
||||
@@ -52,11 +91,9 @@ public class AppInstance {
|
||||
|
||||
var client = tryEstablishConnection(port);
|
||||
if (client.isEmpty()) {
|
||||
// If an instance is running as another user, we cannot connect to it as the xpipe_auth file is inaccessible
|
||||
// Therefore the beacon client is not present.
|
||||
// We still should check whether it is somehow occupied, otherwise beacon server startup will fail
|
||||
TrackEvent.info(
|
||||
"Another instance is already running on this port as another user or is not reachable. Quitting ...");
|
||||
"Another instance is already running on this port but is not reachable. Quitting ...");
|
||||
AppOperationMode.halt(1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -236,8 +236,15 @@ public class AppLayoutModel {
|
||||
@AllArgsConstructor
|
||||
public static class QueueEntry {
|
||||
|
||||
public static QueueEntry ofNotification(String key, String value) {
|
||||
return new QueueEntry(AppI18n.observable(key), new LabelGraphic.IconGraphic(value), () -> true);
|
||||
public static QueueEntry ofNotification(String nameKey, String icon) {
|
||||
return new QueueEntry(AppI18n.observable(nameKey), new LabelGraphic.IconGraphic(icon), () -> true);
|
||||
}
|
||||
|
||||
public static QueueEntry ofNotification(String nameKey, String modalKey, String icon, boolean hideOnClick) {
|
||||
return new QueueEntry(AppI18n.observable(nameKey), new LabelGraphic.IconGraphic(icon), () -> {
|
||||
AppDialog.information(modalKey);
|
||||
return hideOnClick;
|
||||
});
|
||||
}
|
||||
|
||||
public QueueEntry(ObservableValue<String> name, LabelGraphic icon, Supplier<Boolean> action) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package io.xpipe.app.core;
|
||||
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.process.LocalShell;
|
||||
import io.xpipe.app.process.ShellDialect;
|
||||
import io.xpipe.app.process.ShellDialects;
|
||||
import io.xpipe.app.process.*;
|
||||
import io.xpipe.app.update.AppDistributionType;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
@@ -17,27 +15,24 @@ public class AppRestart {
|
||||
var loc = AppProperties.get().isDevelopmentEnvironment()
|
||||
? AppInstallation.ofDefault()
|
||||
: AppInstallation.ofCurrent();
|
||||
var suffix = (arguments.size() > 0 ? " " + String.join(" ", arguments) : "");
|
||||
if (AppDistributionType.get() == AppDistributionType.APP_IMAGE) {
|
||||
var exec = System.getenv("APPIMAGE");
|
||||
return "nohup \"" + exec + "\"" + suffix + " </dev/null >/dev/null 2>&1 & disown";
|
||||
var b = CommandBuilder.of().addQuoted(exec).addAll(arguments);
|
||||
var async = dialect.launchAsync(b, true);
|
||||
return async.buildSimple();
|
||||
} else if (OsType.ofLocal() == OsType.LINUX) {
|
||||
var exec = loc.getCliExecutablePath();
|
||||
return "\"" + exec + "\" open" + suffix;
|
||||
var b = CommandBuilder.of().addFile(exec).add("open").addAll(arguments);
|
||||
return b.buildSimple();
|
||||
} else if (OsType.ofLocal() == OsType.MACOS) {
|
||||
var exec = loc.getCliExecutablePath();
|
||||
return "\"" + exec + "\" open" + suffix;
|
||||
var b = CommandBuilder.of().addFile(exec).add("open").addAll(arguments);
|
||||
return b.buildSimple();
|
||||
} else {
|
||||
var exe = loc.getDaemonExecutablePath();
|
||||
if (ShellDialects.isPowershell(dialect)) {
|
||||
var escapedList =
|
||||
arguments.stream().map(s -> s.replaceAll("\"", "`\"")).toList();
|
||||
var argumentList = String.join(" ", escapedList);
|
||||
return "echo \"Starting XPipe ...\"; Start-Process -FilePath \"" + exe + "\" -ArgumentList \"" + argumentList + "\"";
|
||||
} else {
|
||||
var base = "\"" + exe + "\"" + suffix;
|
||||
return "echo Starting XPipe ...&start \"\" " + base;
|
||||
}
|
||||
var b = CommandBuilder.of().addFile(exe).addAll(arguments);
|
||||
var async = dialect.launchAsync(b, true);
|
||||
return async.buildSimple();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,26 +40,29 @@ public class AppRestart {
|
||||
var loc = AppProperties.get().isDevelopmentEnvironment()
|
||||
? AppInstallation.ofDefault()
|
||||
: AppInstallation.ofCurrent();
|
||||
var suffix = (arguments.size() > 0 ? " " + String.join(" ", arguments) : "");
|
||||
if (AppDistributionType.get() == AppDistributionType.APP_IMAGE) {
|
||||
var exec = System.getenv("APPIMAGE");
|
||||
return "nohup \"" + exec + "\"" + suffix + " </dev/null >/dev/null 2>&1 & disown";
|
||||
var b = CommandBuilder.of().addQuoted(exec).addAll(arguments);
|
||||
var async = dialect.launchAsync(b, true);
|
||||
return async.buildSimple();
|
||||
} else if (OsType.ofLocal() == OsType.LINUX) {
|
||||
return "nohup \"" + loc.getDaemonExecutablePath() + "\"" + suffix + " </dev/null >/dev/null 2>&1 & disown";
|
||||
var exec = loc.getDaemonExecutablePath();
|
||||
var b = CommandBuilder.of().addFile(exec).addAll(arguments);
|
||||
var async = dialect.launchAsync(b, true);
|
||||
return async.buildSimple();
|
||||
} else if (OsType.ofLocal() == OsType.MACOS) {
|
||||
return "(sleep 1;open \"" + loc.getBaseInstallationPath() + "\" --args" + suffix
|
||||
+ " </dev/null &>/dev/null) & disown";
|
||||
var exec = loc.getBaseInstallationPath();
|
||||
var b = CommandBuilder.of()
|
||||
.add("open", "-na")
|
||||
.addFile(exec)
|
||||
.addIf(!arguments.isEmpty(), "--args")
|
||||
.addAll(arguments);
|
||||
return b.buildSimple();
|
||||
} else {
|
||||
var exe = loc.getDaemonExecutablePath();
|
||||
if (ShellDialects.isPowershell(dialect)) {
|
||||
var escapedList =
|
||||
arguments.stream().map(s -> s.replaceAll("\"", "`\"")).toList();
|
||||
var argumentList = String.join(" ", escapedList);
|
||||
return "Start-Process -FilePath \"" + exe + "\" -ArgumentList \"" + argumentList + "\"";
|
||||
} else {
|
||||
var base = "\"" + exe + "\"" + suffix;
|
||||
return "start \"\" " + base;
|
||||
}
|
||||
var b = CommandBuilder.of().addFile(exe).addAll(arguments);
|
||||
var async = dialect.launchAsync(b, true);
|
||||
return async.buildSimple();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ public abstract class AppSystemInfo {
|
||||
var r = Shell32Util.getKnownFolderPath(KnownFolders.FOLDERID_Downloads);
|
||||
// Replace 8.3 filename
|
||||
return (downloads = Path.of(r).toRealPath());
|
||||
} catch (Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
if (!(e instanceof Win32Exception)) {
|
||||
ErrorEventFactory.fromThrowable(e).handle();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package io.xpipe.app.core.check;
|
||||
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.util.LocalExec;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
public class AppHardwareAccelerationDisableCheck {
|
||||
|
||||
@@ -64,6 +64,7 @@ public class AppBaseMode extends AppOperationMode {
|
||||
// if (true) throw new IllegalStateException();
|
||||
|
||||
TrackEvent.info("Initializing base mode components ...");
|
||||
AppCertStore.init();
|
||||
AppMainWindow.loadingText("checkingLicense");
|
||||
LicenseProvider.get().init();
|
||||
AppMainWindow.loadingText("initializingApp");
|
||||
@@ -157,6 +158,7 @@ public class AppBaseMode extends AppOperationMode {
|
||||
TerminalView.init();
|
||||
TerminalLauncherManager.init();
|
||||
TerminalDockHubManager.init();
|
||||
RemoteDesktopWindow.init();
|
||||
TrackEvent.info("File/Terminal initialization thread completed");
|
||||
},
|
||||
() -> {
|
||||
@@ -182,7 +184,7 @@ public class AppBaseMode extends AppOperationMode {
|
||||
TrackEvent.info("Browser initialization thread completed");
|
||||
});
|
||||
|
||||
AppGreetingsDialog.showAndWaitIfNeeded();
|
||||
// AppGreetingsDialog.showAndWaitIfNeeded();
|
||||
TrackEvent.info("Waiting for startup dialogs to close");
|
||||
AppDialog.waitForAllDialogsClose();
|
||||
UpdateChangelogDialog.showIfNeeded();
|
||||
@@ -206,6 +208,7 @@ public class AppBaseMode extends AppOperationMode {
|
||||
AbstractAction.reset();
|
||||
AppMcpServer.reset();
|
||||
WorkspaceManager.reset();
|
||||
RemoteDesktopWindow.reset();
|
||||
AppPrefs.reset();
|
||||
DataStorage.reset();
|
||||
DataStorageSyncHandler.getInstance().reset();
|
||||
@@ -227,6 +230,7 @@ public class AppBaseMode extends AppOperationMode {
|
||||
AppDataLock.unlock();
|
||||
BlobManager.reset();
|
||||
FileBridge.reset();
|
||||
AppCertStore.reset();
|
||||
AppFileWatcher.reset();
|
||||
GlobalTimer.reset();
|
||||
LocalFileTracker.reset();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.xpipe.app.core.mode;
|
||||
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.platform.PlatformInit;
|
||||
@@ -25,16 +26,23 @@ public class AppGuiMode extends AppOperationMode {
|
||||
|
||||
@Override
|
||||
public void onSwitchFrom() {
|
||||
// If we are in an externally started shutdown hook, don't close the windows until the platform exits
|
||||
// That way, it is kept open to block for shutdowns on Windows systems
|
||||
if (OsType.ofLocal() != OsType.WINDOWS || !AppOperationMode.isInShutdownHook()) {
|
||||
PlatformThread.runLaterIfNeededBlocking(() -> {
|
||||
TrackEvent.info("Closing windows");
|
||||
Stage.getWindows().stream().toList().forEach(w -> {
|
||||
w.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
TrackEvent.info("Closing windows");
|
||||
PlatformThread.runLaterIfNeededBlocking(() -> {
|
||||
// Close dialogs
|
||||
AppDialog.getModalOverlays().clear();
|
||||
|
||||
// Close other windows
|
||||
Stage.getWindows().stream()
|
||||
.filter(w -> !w.equals(AppMainWindow.get().getStage()))
|
||||
.toList()
|
||||
.forEach(w -> w.hide());
|
||||
|
||||
// If we are in an externally started shutdown hook, don't close the windows until the platform exits
|
||||
// That way, it is kept open to block for shutdowns on Windows systems
|
||||
if (OsType.ofLocal() != OsType.WINDOWS || !AppOperationMode.isInShutdownHook()) {
|
||||
AppMainWindow.get().hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.xpipe.app.platform.PlatformState;
|
||||
import io.xpipe.app.platform.PlatformThreadWatcher;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.prefs.CloseBehaviour;
|
||||
import io.xpipe.app.prefs.CloseBehaviourDialog;
|
||||
import io.xpipe.app.process.LocalShell;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.update.AppDistributionType;
|
||||
@@ -120,7 +121,6 @@ public abstract class AppOperationMode {
|
||||
AppExtensionManager.init();
|
||||
AppI18n.init();
|
||||
AppPrefs.initLocal();
|
||||
AppBeaconServer.setupPort();
|
||||
AppInstance.init();
|
||||
// Initialize early to load in parallel
|
||||
PlatformInit.init(false);
|
||||
@@ -281,7 +281,7 @@ public abstract class AppOperationMode {
|
||||
|
||||
public static void onWindowClose() {
|
||||
CloseBehaviour action;
|
||||
if (AppPrefs.get() != null && !isInStartup() && !isInShutdown()) {
|
||||
if (AppPrefs.get() != null && !isInStartup() && !isInShutdown() && !CloseBehaviourDialog.showIfNeeded()) {
|
||||
action = AppPrefs.get().closeBehaviour().getValue();
|
||||
} else {
|
||||
action = CloseBehaviour.QUIT;
|
||||
|
||||
@@ -6,12 +6,11 @@ import io.xpipe.app.core.*;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.platform.NativeWinWindowControl;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.prefs.CloseBehaviourDialog;
|
||||
import io.xpipe.app.update.AppDistributionType;
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
import io.xpipe.app.util.NativeWinWindowControl;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.application.Platform;
|
||||
@@ -23,7 +22,6 @@ import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@@ -108,7 +106,7 @@ public class AppMainWindow {
|
||||
var scene = new Scene(content, -1, -1, false);
|
||||
content.prefWidthProperty().bind(scene.widthProperty());
|
||||
content.prefHeightProperty().bind(scene.heightProperty());
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
AppWindowStyle.setSceneFill(scene);
|
||||
|
||||
stage.setScene(scene);
|
||||
if (AppPrefs.get() != null) {
|
||||
@@ -118,7 +116,7 @@ public class AppMainWindow {
|
||||
AppWindowStyle.addStylesheets(stage.getScene());
|
||||
AppWindowStyle.addClickShield(stage);
|
||||
AppWindowStyle.addMaximizedPseudoClass(stage);
|
||||
AppWindowStyle.addFontSize(stage);
|
||||
AppWindowStyle.addFontSize(scene);
|
||||
AppTheme.initThemeHandlers(stage);
|
||||
|
||||
AppWindowTitle.getTitle().subscribe(s -> {
|
||||
@@ -167,7 +165,7 @@ public class AppMainWindow {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
public synchronized void show() {
|
||||
stage.show();
|
||||
|
||||
if (OsType.ofLocal() == OsType.WINDOWS) {
|
||||
@@ -294,23 +292,8 @@ public class AppMainWindow {
|
||||
});
|
||||
|
||||
stage.setOnCloseRequest(e -> {
|
||||
if (!AppOperationMode.isInStartup()
|
||||
&& !AppOperationMode.isInShutdown()
|
||||
&& !CloseBehaviourDialog.showIfNeeded()) {
|
||||
e.consume();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close dialogs
|
||||
AppDialog.getModalOverlays().clear();
|
||||
|
||||
// Close other windows
|
||||
Stage.getWindows().stream().filter(w -> !w.equals(stage)).toList().forEach(w -> w.fireEvent(e));
|
||||
|
||||
// Close self
|
||||
stage.close();
|
||||
AppOperationMode.onWindowClose();
|
||||
e.consume();
|
||||
AppOperationMode.onWindowClose();
|
||||
});
|
||||
|
||||
stage.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package io.xpipe.app.core.window;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.NativeMacOsWindowControl;
|
||||
import io.xpipe.app.platform.NativeWinWindowControl;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.NativeMacOsWindowControl;
|
||||
import io.xpipe.app.util.NativeWinWindowControl;
|
||||
import io.xpipe.app.util.RemoteDesktopWindow;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.animation.PauseTransition;
|
||||
@@ -126,7 +127,8 @@ public class AppModifiedStage extends Stage {
|
||||
if (AppPrefs.get().performanceMode().get()
|
||||
|| !mergeFrame()
|
||||
|| AppMainWindow.get() == null
|
||||
|| stage != AppMainWindow.get().getStage()) {
|
||||
|| (stage != AppMainWindow.get().getStage()
|
||||
&& stage != RemoteDesktopWindow.get().getStage())) {
|
||||
seamlessFrame = false;
|
||||
} else {
|
||||
// This is not available on Windows 10
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.xpipe.app.core.window;
|
||||
|
||||
import io.xpipe.app.platform.PlatformInit;
|
||||
import io.xpipe.app.util.RemoteDesktopWindow;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
@@ -9,7 +10,7 @@ import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -39,7 +40,7 @@ public class AppSideWindow {
|
||||
event.consume();
|
||||
});
|
||||
AppWindowBounds.fixInvalidStagePosition(s);
|
||||
AppWindowStyle.addFontSize(s);
|
||||
AppWindowStyle.addFontSize(s.getScene());
|
||||
a.getDialogPane().getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
|
||||
s.close();
|
||||
@@ -83,10 +84,15 @@ public class AppSideWindow {
|
||||
|
||||
public static Alert createEmptyAlert() {
|
||||
Alert alert = new Alert(Alert.AlertType.NONE);
|
||||
if (AppMainWindow.get() != null && AppMainWindow.get().getStage().isShowing() && !AppMainWindow.get().getStage().isIconified()) {
|
||||
alert.initModality(Modality.NONE);
|
||||
if (AppMainWindow.get() != null
|
||||
&& AppMainWindow.get().getStage().isShowing()
|
||||
&& !AppMainWindow.get().getStage().isIconified()
|
||||
&& (RemoteDesktopWindow.get().getStage() == null
|
||||
|| !RemoteDesktopWindow.get().getStage().isShowing())) {
|
||||
alert.initOwner(AppMainWindow.get().getStage());
|
||||
}
|
||||
alert.getDialogPane().getScene().setFill(Color.TRANSPARENT);
|
||||
AppWindowStyle.setSceneFill(alert.getDialogPane().getScene());
|
||||
var stage = (Stage) alert.getDialogPane().getScene().getWindow();
|
||||
AppModifiedStage.prepareStage(stage);
|
||||
AppWindowStyle.addIcons(stage);
|
||||
|
||||
@@ -2,15 +2,18 @@ package io.xpipe.app.core.window;
|
||||
|
||||
import io.xpipe.app.core.*;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -19,6 +22,23 @@ import java.util.List;
|
||||
|
||||
public class AppWindowStyle {
|
||||
|
||||
public static void setSceneFill(Scene scene) {
|
||||
if (OsType.ofLocal() != OsType.LINUX) {
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
return;
|
||||
}
|
||||
|
||||
scene.fillProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
return AppPrefs.get() != null
|
||||
&& AppPrefs.get().theme().getValue().isDark()
|
||||
? Color.BLACK
|
||||
: Color.WHITE;
|
||||
},
|
||||
AppPrefs.get().theme()));
|
||||
}
|
||||
|
||||
public static void addMaximizedPseudoClass(Stage stage) {
|
||||
stage.getScene().rootProperty().subscribe(root -> {
|
||||
stage.maximizedProperty().subscribe(v -> {
|
||||
@@ -27,8 +47,8 @@ public class AppWindowStyle {
|
||||
});
|
||||
}
|
||||
|
||||
public static void addFontSize(Stage stage) {
|
||||
stage.getScene().rootProperty().subscribe(root -> {
|
||||
public static void addFontSize(Scene scene) {
|
||||
scene.rootProperty().subscribe(root -> {
|
||||
AppFontSizes.base(root);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.comp.RegionBuilder;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppSystemInfo;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.BindingsHelper;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.OptionsChoiceBuilder;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.secret.SecretRetrievalStrategy;
|
||||
import io.xpipe.app.secret.SecretStrategyChoiceConfig;
|
||||
import io.xpipe.app.storage.ContextualFileReference;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.KeyValue;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
@JsonTypeName("certificateFile")
|
||||
@AllArgsConstructor
|
||||
public class CertificateKeyFileStrategy implements SshIdentityStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static String getOptionsNameKey() {
|
||||
return "certificateFile";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(
|
||||
Property<CertificateKeyFileStrategy> p, SshIdentityStrategyChoiceConfig config) {
|
||||
var keyPath = new SimpleObjectProperty<>(
|
||||
p.getValue() != null && p.getValue().getFile() != null
|
||||
? p.getValue().getFile().toAbsoluteFilePath(null)
|
||||
: null);
|
||||
var keyPasswordProperty =
|
||||
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPassword() : null);
|
||||
|
||||
var certificate = new SimpleObjectProperty<>(
|
||||
p.getValue() != null && p.getValue().getCertificate() != null
|
||||
? p.getValue().getCertificate().toAbsoluteFilePath(null)
|
||||
: null);
|
||||
|
||||
var shortLivedCertImpl = new SimpleObjectProperty<>(p.getValue().getShortLivedCertImpl());
|
||||
var shortLivedCertImplConfig = BindingsHelper.flatMap(
|
||||
shortLivedCertImpl,
|
||||
implValue -> implValue != null
|
||||
? implValue.getCacheableConfiguration().getValue()
|
||||
: new ReadOnlyObjectWrapper<>());
|
||||
|
||||
p.addListener((observable, oldValue, newValue) -> {
|
||||
if (keyPath.get() != null
|
||||
&& newValue != null
|
||||
&& !ContextualFileReference.of(keyPath.get()).equals(newValue.getFile())) {
|
||||
return;
|
||||
}
|
||||
|
||||
keyPath.setValue(
|
||||
newValue != null && newValue.getFile() != null
|
||||
? newValue.getFile().toAbsoluteFilePath(null)
|
||||
: null);
|
||||
});
|
||||
keyPath.addListener((observable, oldValue, newValue) -> {
|
||||
if (certificate.get() != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var baseName = newValue.getExtension().isPresent() ? newValue.getBaseName() : newValue;
|
||||
var pubCert = FilePath.of(baseName + "-cert.pub");
|
||||
var fs =
|
||||
config.getFileSystem() != null && config.getFileSystem().getValue() != null
|
||||
? config.getFileSystem().getValue().getStore()
|
||||
: (ShellStore) DataStorage.get().local().getStore();
|
||||
var ex = fs.getOrStartSession().view().fileExists(pubCert);
|
||||
if (ex) {
|
||||
Platform.runLater(() -> {
|
||||
certificate.set(pubCert);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var passwordChoice = OptionsChoiceBuilder.builder()
|
||||
.allowNull(false)
|
||||
.property(keyPasswordProperty)
|
||||
.customConfiguration(SecretStrategyChoiceConfig.builder()
|
||||
.allowNone(true)
|
||||
.passwordKey("passphrase")
|
||||
.build())
|
||||
.available(SecretRetrievalStrategy.getClasses())
|
||||
.build()
|
||||
.build();
|
||||
|
||||
var certificateField = new ContextualFileReferenceChoiceComp(
|
||||
config.getFileSystem() != null
|
||||
? config.getFileSystem()
|
||||
: new ReadOnlyObjectWrapper<>(DataStorage.get().local().ref()),
|
||||
certificate,
|
||||
null,
|
||||
List.of(),
|
||||
e -> {
|
||||
if (config.getFileSystem() == null) {
|
||||
return e.equals(DataStorage.get().local());
|
||||
}
|
||||
|
||||
var fs = config.getFileSystem().getValue();
|
||||
if (fs == null) {
|
||||
return e.equals(DataStorage.get().local());
|
||||
} else {
|
||||
return e.equals(fs.get());
|
||||
}
|
||||
},
|
||||
false);
|
||||
certificateField.apply(hBox -> {
|
||||
hBox.getChildren().getLast().getStyleClass().remove(Styles.RIGHT_PILL);
|
||||
hBox.getChildren().getLast().getStyleClass().add(Styles.CENTER_PILL);
|
||||
});
|
||||
|
||||
var checkButton = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2i-information-outline"), () -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var fs = config.getFileSystem() != null
|
||||
&& config.getFileSystem().getValue() != null
|
||||
? config.getFileSystem().getValue().getStore()
|
||||
: (ShellStore) DataStorage.get().local().getStore();
|
||||
ShortLivedCertificateImpl.showDialogAndWait(
|
||||
keyPath.get(), certificate.get(), shortLivedCertImpl.get());
|
||||
});
|
||||
})
|
||||
.describe(d -> d.nameKey("checkValidity"))
|
||||
.disable(certificate.isNull());
|
||||
|
||||
var certificateBox = new InputGroupComp(List.of(certificateField, checkButton));
|
||||
certificateBox.setMainReference(certificateField);
|
||||
|
||||
var implChoice = OptionsChoiceBuilder.builder()
|
||||
.property(shortLivedCertImpl)
|
||||
.allowNull(true)
|
||||
.available(ShortLivedCertificateImpl.getClasses())
|
||||
.transformer(entryComboBox -> {
|
||||
var button = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2w-wrench-outline"), () -> {
|
||||
shortLivedCertImpl.get().configure();
|
||||
});
|
||||
button.describe(d -> d.nameKey("configure"));
|
||||
button.disable(
|
||||
BindingsHelper.mapBoolean(shortLivedCertImpl, v -> v == null || !v.supportsConfigure()));
|
||||
var hbox = new InputGroupComp(List.of(RegionBuilder.of(() -> entryComboBox), button))
|
||||
.setMainReference(0)
|
||||
.build();
|
||||
return hbox;
|
||||
})
|
||||
.build();
|
||||
|
||||
return new OptionsBuilder()
|
||||
.name("location")
|
||||
.description("locationDescription")
|
||||
.addComp(
|
||||
new ContextualFileReferenceChoiceComp(
|
||||
config.getFileSystem() != null
|
||||
? config.getFileSystem()
|
||||
: new ReadOnlyObjectWrapper<>(
|
||||
DataStorage.get().local().ref()),
|
||||
keyPath,
|
||||
null,
|
||||
List.of(),
|
||||
e -> {
|
||||
if (config.getFileSystem() == null) {
|
||||
return e.equals(DataStorage.get().local());
|
||||
}
|
||||
|
||||
var fs = config.getFileSystem().getValue();
|
||||
if (fs == null) {
|
||||
return e.equals(DataStorage.get().local());
|
||||
} else {
|
||||
return e.equals(fs.get());
|
||||
}
|
||||
},
|
||||
false),
|
||||
keyPath)
|
||||
.nonNull()
|
||||
.nameAndDescription("keyPassphrase")
|
||||
.sub(passwordChoice, keyPasswordProperty)
|
||||
.nonNull()
|
||||
.nameAndDescription("certificatePublicKey")
|
||||
.addComp(certificateBox, certificate)
|
||||
.nonNull()
|
||||
.nameAndDescription("shortLivedCertImpl")
|
||||
.sub(implChoice.build(), shortLivedCertImpl)
|
||||
.addProperty(shortLivedCertImplConfig)
|
||||
.checkComplete()
|
||||
.bind(
|
||||
() -> {
|
||||
return new CertificateKeyFileStrategy(
|
||||
ContextualFileReference.of(keyPath.get()),
|
||||
keyPasswordProperty.get(),
|
||||
ContextualFileReference.of(certificate.get()),
|
||||
shortLivedCertImpl.get());
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
ContextualFileReference file;
|
||||
SecretRetrievalStrategy password;
|
||||
ContextualFileReference certificate;
|
||||
ShortLivedCertificateImpl shortLivedCertImpl;
|
||||
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(file);
|
||||
Validators.nonNull(password);
|
||||
Validators.nonNull(certificate);
|
||||
if (shortLivedCertImpl != null) {
|
||||
shortLivedCertImpl.checkComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareParent(ShellControl parent) throws Exception {
|
||||
parent.requireLicensedFeature(LicenseProvider.get().getFeature("sshCertificateFile"));
|
||||
preparePrivateKey(parent);
|
||||
prepareCertificateKey(parent, false);
|
||||
}
|
||||
|
||||
private void preparePrivateKey(ShellControl parent) throws Exception {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var s = file.toAbsoluteFilePath(parent).resolveTildeHome(parent.view().userHome());
|
||||
if (!parent.view().fileExists(s)) {
|
||||
var systemName = parent.getSourceStore()
|
||||
.flatMap(shellStore -> DataStorage.get().getStoreEntryIfPresent(shellStore, false))
|
||||
.map(e -> DataStorage.get().getStoreEntryDisplayName(e));
|
||||
var msg = "Private key file " + s + " does not exist"
|
||||
+ (systemName.isPresent() ? " on system " + systemName.get() : "");
|
||||
throw ErrorEventFactory.expected(new IllegalArgumentException(msg));
|
||||
}
|
||||
|
||||
if (s.toString().endsWith(".pub")) {
|
||||
throw ErrorEventFactory.expected(new IllegalArgumentException("Identity file " + s
|
||||
+ " is marked to be a public key file, SSH authentication requires the private key"));
|
||||
}
|
||||
|
||||
if (parent.getOsType() != OsType.WINDOWS) {
|
||||
// Try to preserve the same permission set
|
||||
parent.command(CommandBuilder.of()
|
||||
.add("test", "-w")
|
||||
.addFile(s)
|
||||
.add("&&", "chmod", "600")
|
||||
.addFile(s)
|
||||
.add("||", "chmod", "400")
|
||||
.addFile(s))
|
||||
.executeAndCheck();
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareCertificateKey(ShellControl parent, boolean alreadyRenewed) throws Exception {
|
||||
if (certificate == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var s = certificate
|
||||
.toAbsoluteFilePath(parent)
|
||||
.resolveTildeHome(parent.view().userHome());
|
||||
if (parent.view().fileExists(s)) {
|
||||
if (parent.getOsType() != OsType.WINDOWS) {
|
||||
// Try to preserve the same permission set
|
||||
parent.command(CommandBuilder.of()
|
||||
.add("test", "-w")
|
||||
.addFile(s)
|
||||
.add("&&", "chmod", "600")
|
||||
.addFile(s)
|
||||
.add("||", "chmod", "400")
|
||||
.addFile(s))
|
||||
.executeAndCheck();
|
||||
}
|
||||
|
||||
var summary = ShortLivedCertificateImpl.queryCertificateSummary(parent, s);
|
||||
var valid = ShortLivedCertificateImpl.checkValid(summary);
|
||||
|
||||
if (!valid) {
|
||||
var pubKey = SshIdentityStrategy.getPublicKeyPath(file.toAbsoluteFilePath(parent)
|
||||
.resolveTildeHome(parent.view().userHome()));
|
||||
if (!parent.view().fileExists(pubKey)) {
|
||||
var systemName = parent.getSourceStore()
|
||||
.flatMap(shellStore -> DataStorage.get().getStoreEntryIfPresent(shellStore, false))
|
||||
.map(e -> DataStorage.get().getStoreEntryDisplayName(e));
|
||||
var msg = "Public key file " + pubKey + " does not exist"
|
||||
+ (systemName.isPresent() ? " on system " + systemName.get() : "");
|
||||
throw ErrorEventFactory.expected(new IllegalArgumentException(msg));
|
||||
}
|
||||
|
||||
if (!alreadyRenewed
|
||||
&& parent.isLocal()
|
||||
&& shortLivedCertImpl != null
|
||||
&& shortLivedCertImpl.isComplete()
|
||||
&& shortLivedCertImpl.supportsRenew()) {
|
||||
ShortLivedCertificateImpl.showDialogAndWait(
|
||||
file.toAbsoluteFilePath(parent)
|
||||
.resolveTildeHome(parent.view().userHome()),
|
||||
s,
|
||||
shortLivedCertImpl);
|
||||
prepareCertificateKey(parent, true);
|
||||
} else {
|
||||
throw ErrorEventFactory.expected(new IllegalStateException("Certificate " + s.getFileName()
|
||||
+ " is expired" + (alreadyRenewed ? " and failed to renew" : "")));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!alreadyRenewed
|
||||
&& parent.isLocal()
|
||||
&& shortLivedCertImpl != null
|
||||
&& shortLivedCertImpl.isComplete()
|
||||
&& shortLivedCertImpl.supportsRenew()) {
|
||||
shortLivedCertImpl.renew(
|
||||
file.toAbsoluteFilePath(parent)
|
||||
.resolveTildeHome(parent.view().userHome()),
|
||||
s);
|
||||
prepareCertificateKey(parent, true);
|
||||
} else {
|
||||
throw ErrorEventFactory.expected(
|
||||
new IllegalStateException("Certificate file " + s + " does not exist"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildCommand(CommandBuilder builder) {}
|
||||
|
||||
@Override
|
||||
public List<KeyValue> configOptions(ShellControl sc) {
|
||||
return List.of(
|
||||
KeyValue.raw("IdentitiesOnly", "yes"),
|
||||
KeyValue.raw("IdentityAgent", "none"),
|
||||
KeyValue.escape("IdentityFile", resolveFilePath(sc, file)),
|
||||
KeyValue.escape("CertificateFile", resolveFilePath(sc, certificate)),
|
||||
KeyValue.raw("PKCS11Provider", "none"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecretRetrievalStrategy getAskpassStrategy() {
|
||||
return password;
|
||||
}
|
||||
|
||||
private FilePath resolveFilePath(ShellControl sc, ContextualFileReference f) {
|
||||
var s = f.toAbsoluteFilePath(sc);
|
||||
// The ~ is supported on all platforms, so manually replace it here for Windows
|
||||
if (s.startsWith("~")) {
|
||||
s = s.resolveTildeHome(FilePath.of(AppSystemInfo.ofCurrent().getUserHome()));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsIdentityApply() {
|
||||
// This is managed by the server in the trusted user ca keys
|
||||
return false;
|
||||
}
|
||||
|
||||
public PublicKeyStrategy getPublicKeyStrategy() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.comp.base.HorizontalComp;
|
||||
import io.xpipe.app.comp.base.TextFieldComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.Validator;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.KeyValue;
|
||||
import io.xpipe.core.OsType;
|
||||
@@ -85,7 +87,7 @@ public class CustomAgentStrategy implements SshIdentityAgentStrategy {
|
||||
}))
|
||||
.nameAndDescription("publicKey")
|
||||
.documentationLink(DocumentationLink.SSH_AGENT_PUBLIC_KEYS)
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false, false), publicKey)
|
||||
.bind(
|
||||
() -> {
|
||||
return new CustomAgentStrategy(publicKey.get());
|
||||
@@ -95,6 +97,11 @@ public class CustomAgentStrategy implements SshIdentityAgentStrategy {
|
||||
|
||||
String publicKey;
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(AppPrefs.get().defaultSshAgentSocket().getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareParent(ShellControl parent) throws Exception {
|
||||
if (parent.isLocal()) {
|
||||
@@ -107,7 +114,7 @@ public class CustomAgentStrategy implements SshIdentityAgentStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePath determinetAgentSocketLocation(ShellControl sc) throws Exception {
|
||||
public FilePath determineAgentSocketLocation(ShellControl sc) throws Exception {
|
||||
if (!sc.isLocal() || sc.getOsType() == OsType.WINDOWS) {
|
||||
return null;
|
||||
}
|
||||
@@ -132,13 +139,13 @@ public class CustomAgentStrategy implements SshIdentityAgentStrategy {
|
||||
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
|
||||
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
|
||||
var l = new ArrayList<>(List.of(
|
||||
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
|
||||
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
|
||||
new KeyValue("PKCS11Provider", "none")));
|
||||
KeyValue.raw("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
|
||||
KeyValue.raw("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
|
||||
KeyValue.raw("PKCS11Provider", "none")));
|
||||
|
||||
var agent = determinetAgentSocketLocation(sc);
|
||||
var agent = determineAgentSocketLocation(sc);
|
||||
if (agent != null) {
|
||||
l.add(new KeyValue("IdentityAgent", "\"" + agent + "\""));
|
||||
l.add(KeyValue.escape("IdentityAgent", agent));
|
||||
}
|
||||
|
||||
return l;
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.KeyValue;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
@JsonTypeName("customPkcs11")
|
||||
@AllArgsConstructor
|
||||
public class CustomPkcs11LibraryStrategy implements SshIdentityStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static String getOptionsNameKey() {
|
||||
return "customPkcs11Library";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(
|
||||
Property<CustomPkcs11LibraryStrategy> p, SshIdentityStrategyChoiceConfig config) {
|
||||
|
||||
var file =
|
||||
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getFile() : null);
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("pkcs11Library")
|
||||
.addComp(
|
||||
new ContextualFileReferenceChoiceComp(
|
||||
config.getFileSystem() != null
|
||||
? config.getFileSystem()
|
||||
: new ReadOnlyObjectWrapper<>(
|
||||
DataStorage.get().local().ref()),
|
||||
file,
|
||||
null,
|
||||
List.of(),
|
||||
e -> {
|
||||
if (config.getFileSystem() == null) {
|
||||
return e.equals(DataStorage.get().local());
|
||||
}
|
||||
|
||||
var fs = config.getFileSystem().getValue();
|
||||
if (fs == null) {
|
||||
return e.equals(DataStorage.get().local());
|
||||
} else {
|
||||
return e.equals(fs.get());
|
||||
}
|
||||
},
|
||||
false),
|
||||
file)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
return new CustomPkcs11LibraryStrategy(file.get());
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
FilePath file;
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareParent(ShellControl parent) throws Exception {
|
||||
parent.requireLicensedFeature(LicenseProvider.get().getFeature("pkcs11Identity"));
|
||||
|
||||
if (!parent.getShellDialect()
|
||||
.createFileExistsCommand(parent, file.toString())
|
||||
.executeAndCheck()) {
|
||||
throw ErrorEventFactory.expected(new IOException("PKCS11 library at " + file + " not found"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildCommand(CommandBuilder builder) {
|
||||
builder.setup(sc -> {
|
||||
var dir = file.getParent();
|
||||
if (sc.getOsType() == OsType.WINDOWS) {
|
||||
builder.addToPath(dir, true);
|
||||
} else {
|
||||
builder.addToEnvironmentPath("LD_LIBRARY_PATH", dir, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyValue> configOptions(ShellControl sc) {
|
||||
return List.of(
|
||||
new KeyValue("IdentitiesOnly", "no"),
|
||||
new KeyValue("PKCS11Provider", "\"" + file.toString() + "\""),
|
||||
new KeyValue("IdentityFile", "none"),
|
||||
new KeyValue("IdentityAgent", "none"));
|
||||
}
|
||||
|
||||
public PublicKeyStrategy getPublicKeyStrategy() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -36,12 +36,7 @@ public class GpgAgentStrategy implements SshIdentityAgentStrategy {
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("publicKey")
|
||||
.documentationLink(DocumentationLink.SSH_AGENT_PUBLIC_KEYS)
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
|
||||
.addComp(
|
||||
new TextFieldComp(publicKey)
|
||||
.apply(struc -> struc.setPromptText(
|
||||
"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment")),
|
||||
publicKey)
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false, false), publicKey)
|
||||
.bind(
|
||||
() -> {
|
||||
return new GpgAgentStrategy(publicKey.get());
|
||||
@@ -67,16 +62,18 @@ public class GpgAgentStrategy implements SshIdentityAgentStrategy {
|
||||
|
||||
String publicKey;
|
||||
|
||||
@Override
|
||||
public void checkComplete() {}
|
||||
|
||||
@Override
|
||||
public void prepareParent(ShellControl parent) throws Exception {
|
||||
parent.requireLicensedFeature(LicenseProvider.get().getFeature("gpgAgent"));
|
||||
if (parent.isLocal()) {
|
||||
SshIdentityStateManager.prepareLocalGpgAgent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePath determinetAgentSocketLocation(ShellControl sc) throws Exception {
|
||||
public FilePath determineAgentSocketLocation(ShellControl sc) throws Exception {
|
||||
if (sc.getOsType() == OsType.WINDOWS) {
|
||||
return null;
|
||||
}
|
||||
@@ -96,13 +93,13 @@ public class GpgAgentStrategy implements SshIdentityAgentStrategy {
|
||||
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
|
||||
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
|
||||
var l = new ArrayList<>(List.of(
|
||||
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
|
||||
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
|
||||
new KeyValue("PKCS11Provider", "none")));
|
||||
KeyValue.raw("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
|
||||
KeyValue.escape("IdentityFile", file.isPresent() ? file.get() : "none"),
|
||||
KeyValue.raw("PKCS11Provider", "none")));
|
||||
|
||||
var agent = determinetAgentSocketLocation(sc);
|
||||
var agent = determineAgentSocketLocation(sc);
|
||||
if (agent != null) {
|
||||
l.add(new KeyValue("IdentityAgent", "\"" + agent + "\""));
|
||||
l.add(KeyValue.escape("IdentityAgent", agent));
|
||||
}
|
||||
|
||||
return l;
|
||||
|
||||
@@ -8,10 +8,7 @@ import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppSystemInfo;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.platform.ClipboardHelper;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.OptionsChoiceBuilder;
|
||||
import io.xpipe.app.platform.*;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.secret.SecretRetrievalStrategy;
|
||||
@@ -24,6 +21,7 @@ import io.xpipe.core.*;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -33,8 +31,7 @@ import lombok.extern.jackson.Jacksonized;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Value
|
||||
@@ -44,6 +41,12 @@ import java.util.stream.Collectors;
|
||||
@AllArgsConstructor
|
||||
public class InPlaceKeyStrategy implements SshIdentityStrategy {
|
||||
|
||||
private static final Set<String> KEYS = new HashSet<>();
|
||||
|
||||
public static boolean isInPlaceKey(String keyName) {
|
||||
return KEYS.contains(keyName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<InPlaceKeyStrategy> p, SshIdentityStrategyChoiceConfig config) {
|
||||
var options = new OptionsBuilder();
|
||||
@@ -71,17 +74,26 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
|
||||
AppI18n.activeLanguage()));
|
||||
struc.setEditable(false);
|
||||
});
|
||||
var generatedKeyBase = new SimpleObjectProperty<>(key.get());
|
||||
var generateButtonDisabled = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return key.get() == null
|
||||
|| (publicKey.get() != null && key.get().equals(generatedKeyBase.get()));
|
||||
},
|
||||
key,
|
||||
publicKey);
|
||||
var generateButton = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2c-cog-refresh-outline"), () -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
var generated = ProcessControlProvider.get()
|
||||
.generatePublicSshKey(InPlaceSecretValue.of(key.get()), keyPasswordProperty.get());
|
||||
if (generated != null) {
|
||||
publicKey.set(generated);
|
||||
generatedKeyBase.set(key.getValue());
|
||||
}
|
||||
});
|
||||
})
|
||||
.describe(d -> d.nameKey("generatePublicKey"))
|
||||
.disable(key.isNull().or(publicKey.isNotNull()).or(keyPasswordProperty.isNull()));
|
||||
.disable(generateButtonDisabled);
|
||||
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
|
||||
ClipboardHelper.copyText(publicKey.get());
|
||||
})
|
||||
@@ -104,8 +116,7 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
|
||||
}),
|
||||
key)
|
||||
.nonNull()
|
||||
.name("keyPassword")
|
||||
.description("sshConfigHost.identityPassphraseDescription")
|
||||
.nameAndDescription("keyPassphrase")
|
||||
.sub(passwordChoice, keyPasswordProperty)
|
||||
.nonNull()
|
||||
.nameAndDescription("inPlacePublicKey")
|
||||
@@ -167,10 +178,10 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
|
||||
@Override
|
||||
public List<KeyValue> configOptions(ShellControl sc) {
|
||||
return List.of(
|
||||
new KeyValue("IdentitiesOnly", "yes"),
|
||||
new KeyValue("IdentityAgent", "none"),
|
||||
new KeyValue("IdentityFile", "\"" + getTargetFilePath(sc) + "\""),
|
||||
new KeyValue("PKCS11Provider", "none"));
|
||||
KeyValue.raw("IdentitiesOnly", "yes"),
|
||||
KeyValue.raw("IdentityAgent", "none"),
|
||||
KeyValue.escape("IdentityFile", getTargetFilePath(sc)),
|
||||
KeyValue.raw("PKCS11Provider", "none"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -179,9 +190,9 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
|
||||
}
|
||||
|
||||
private FilePath getTargetFilePath(ShellControl sc) {
|
||||
var temp = sc.getSystemTemporaryDirectory()
|
||||
.join("xpipe-"
|
||||
+ Math.abs(Objects.hash(this, AppSystemInfo.ofCurrent().getUser())) + ".key");
|
||||
var hash = Math.abs(Objects.hash(this, AppSystemInfo.ofCurrent().getUser()));
|
||||
var temp = sc.getSystemTemporaryDirectory().join("xpipe-" + hash + ".key");
|
||||
KEYS.add(temp.getFileName());
|
||||
return temp;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppSystemInfo;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.ClipboardHelper;
|
||||
@@ -20,7 +20,6 @@ import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.InPlaceSecretValue;
|
||||
import io.xpipe.core.KeyValue;
|
||||
import io.xpipe.core.OsType;
|
||||
@@ -105,12 +104,12 @@ public class KeyFileStrategy implements SshIdentityStrategy {
|
||||
var sc = config.getFileSystem() != null
|
||||
? config.getFileSystem().getValue().getStore().getOrStartSession()
|
||||
: LocalShell.getShell();
|
||||
var path = keyPath.get();
|
||||
var path = keyPath.get().resolveTildeHome(sc.view().userHome());
|
||||
if (!sc.view().fileExists(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var pubKeyPath = FilePath.of(path + ".pub");
|
||||
var pubKeyPath = SshIdentityStrategy.getPublicKeyPath(path);
|
||||
if (sc.view().fileExists(pubKeyPath)) {
|
||||
var contents = sc.view().readTextFile(pubKeyPath).strip();
|
||||
Platform.runLater(() -> {
|
||||
@@ -140,6 +139,28 @@ public class KeyFileStrategy implements SshIdentityStrategy {
|
||||
var publicKeyBox = new InputGroupComp(List.of(publicKeyField, copyButton, generateButton));
|
||||
publicKeyBox.setMainReference(publicKeyField);
|
||||
|
||||
keyPath.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var pubFile = SshIdentityStrategy.getPublicKeyPath(newValue);
|
||||
var fs =
|
||||
config.getFileSystem() != null && config.getFileSystem().getValue() != null
|
||||
? config.getFileSystem().getValue().getStore()
|
||||
: (ShellStore) DataStorage.get().local().getStore();
|
||||
var ex = fs.getOrStartSession().view().fileExists(pubFile);
|
||||
if (ex) {
|
||||
var contents =
|
||||
fs.getOrStartSession().view().readTextFile(pubFile).strip();
|
||||
Platform.runLater(() -> {
|
||||
publicKey.set(contents);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return new OptionsBuilder()
|
||||
.name("location")
|
||||
.description("locationDescription")
|
||||
@@ -167,8 +188,7 @@ public class KeyFileStrategy implements SshIdentityStrategy {
|
||||
false),
|
||||
keyPath)
|
||||
.nonNull()
|
||||
.name("keyPassword")
|
||||
.description("sshConfigHost.identityPassphraseDescription")
|
||||
.nameAndDescription("keyPassphrase")
|
||||
.sub(passwordChoice, keyPasswordProperty)
|
||||
.nonNull()
|
||||
.nameAndDescription("inPlacePublicKey")
|
||||
@@ -247,12 +267,14 @@ public class KeyFileStrategy implements SshIdentityStrategy {
|
||||
public void buildCommand(CommandBuilder builder) {}
|
||||
|
||||
@Override
|
||||
public List<KeyValue> configOptions(ShellControl sc) {
|
||||
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
|
||||
return List.of(
|
||||
new KeyValue("IdentitiesOnly", "yes"),
|
||||
new KeyValue("IdentityAgent", "none"),
|
||||
new KeyValue("IdentityFile", "\"" + resolveFilePath(sc).toString() + "\""),
|
||||
new KeyValue("PKCS11Provider", "none"));
|
||||
KeyValue.raw("IdentitiesOnly", "yes"),
|
||||
KeyValue.raw("IdentityAgent", "none"),
|
||||
KeyValue.escape(
|
||||
"IdentityFile",
|
||||
file.toAbsoluteFilePath(sc).resolveTildeHome(sc.view().userHome())),
|
||||
KeyValue.raw("PKCS11Provider", "none"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -260,15 +282,6 @@ public class KeyFileStrategy implements SshIdentityStrategy {
|
||||
return password;
|
||||
}
|
||||
|
||||
private FilePath resolveFilePath(ShellControl sc) {
|
||||
var s = file.toAbsoluteFilePath(sc);
|
||||
// The ~ is supported on all platforms, so manually replace it here for Windows
|
||||
if (s.startsWith("~")) {
|
||||
s = s.resolveTildeHome(FilePath.of(AppSystemInfo.ofCurrent().getUserHome()));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
public PublicKeyStrategy getPublicKeyStrategy() {
|
||||
return PublicKeyStrategy.Fixed.of(publicKey);
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ public class NoIdentityStrategy implements SshIdentityStrategy {
|
||||
public List<KeyValue> configOptions(ShellControl sc) {
|
||||
// Don't use any agent keys to prevent too many authentication failures
|
||||
return List.of(
|
||||
new KeyValue("IdentitiesOnly", "yes"),
|
||||
new KeyValue("IdentityAgent", "none"),
|
||||
new KeyValue("IdentityFile", "none"),
|
||||
new KeyValue("PKCS11Provider", "none"));
|
||||
KeyValue.raw("IdentitiesOnly", "yes"),
|
||||
KeyValue.raw("IdentityAgent", "none"),
|
||||
KeyValue.raw("IdentityFile", "none"),
|
||||
KeyValue.raw("PKCS11Provider", "none"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -39,7 +39,7 @@ public class OpenSshAgentStrategy implements SshIdentityAgentStrategy {
|
||||
.hide(OsType.ofLocal() == OsType.WINDOWS)
|
||||
.nameAndDescription("publicKey")
|
||||
.documentationLink(DocumentationLink.SSH_AGENT_PUBLIC_KEYS)
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false, false), publicKey)
|
||||
.bind(
|
||||
() -> {
|
||||
return new OpenSshAgentStrategy(publicKey.get());
|
||||
@@ -58,7 +58,10 @@ public class OpenSshAgentStrategy implements SshIdentityAgentStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePath determinetAgentSocketLocation(ShellControl sc) throws Exception {
|
||||
public void checkComplete() {}
|
||||
|
||||
@Override
|
||||
public FilePath determineAgentSocketLocation(ShellControl sc) throws Exception {
|
||||
if (sc.getOsType() == OsType.WINDOWS) {
|
||||
return null;
|
||||
}
|
||||
@@ -80,13 +83,13 @@ public class OpenSshAgentStrategy implements SshIdentityAgentStrategy {
|
||||
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
|
||||
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
|
||||
var l = new ArrayList<>(List.of(
|
||||
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
|
||||
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
|
||||
new KeyValue("PKCS11Provider", "none")));
|
||||
KeyValue.raw("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
|
||||
KeyValue.escape("IdentityFile", file.isPresent() ? file.get() : "none"),
|
||||
KeyValue.raw("PKCS11Provider", "none")));
|
||||
|
||||
var agent = determinetAgentSocketLocation(sc);
|
||||
var agent = determineAgentSocketLocation(sc);
|
||||
if (agent != null) {
|
||||
l.add(new KeyValue("IdentityAgent", "\"" + agent + "\""));
|
||||
l.add(KeyValue.escape("IdentityAgent", agent));
|
||||
}
|
||||
|
||||
return l;
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.KeyValue;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonTypeName("otherExternal")
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
public class OtherExternalAgentStrategy implements SshIdentityAgentStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(
|
||||
Property<OtherExternalAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {
|
||||
var publicKey =
|
||||
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("publicKey")
|
||||
.documentationLink(DocumentationLink.SSH_AGENT_PUBLIC_KEYS)
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
|
||||
.bind(
|
||||
() -> {
|
||||
return new OtherExternalAgentStrategy(publicKey.get());
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
String publicKey;
|
||||
|
||||
@Override
|
||||
public void prepareParent(ShellControl parent) throws Exception {
|
||||
if (parent.isLocal()) {
|
||||
SshIdentityStateManager.prepareLocalExternalAgent(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePath determinetAgentSocketLocation(ShellControl parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildCommand(CommandBuilder builder) {}
|
||||
|
||||
@Override
|
||||
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
|
||||
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
|
||||
return List.of(
|
||||
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
|
||||
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
|
||||
new KeyValue("PKCS11Provider", "none"));
|
||||
}
|
||||
|
||||
public PublicKeyStrategy getPublicKeyStrategy() {
|
||||
return PublicKeyStrategy.Fixed.of(publicKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.KeyValue;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonTypeName("otherExternal")
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
public class OtherExternalIdentityStrategy implements SshIdentityStrategy {
|
||||
|
||||
@Override
|
||||
public void prepareParent(ShellControl parent) throws Exception {}
|
||||
|
||||
@Override
|
||||
public void checkComplete() {}
|
||||
|
||||
@Override
|
||||
public void buildCommand(CommandBuilder builder) {}
|
||||
|
||||
@Override
|
||||
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
public PublicKeyStrategy getPublicKeyStrategy() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ public class PageantStrategy implements SshIdentityAgentStrategy {
|
||||
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("publicKey")
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false, false), publicKey)
|
||||
.bind(
|
||||
() -> {
|
||||
return new PageantStrategy(publicKey.get());
|
||||
@@ -87,7 +87,7 @@ public class PageantStrategy implements SshIdentityAgentStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePath determinetAgentSocketLocation(ShellControl sc) {
|
||||
public FilePath determineAgentSocketLocation(ShellControl sc) {
|
||||
if (sc.isLocal() && sc.getOsType() == OsType.WINDOWS) {
|
||||
return FilePath.of(getPageantWindowsPipe());
|
||||
}
|
||||
@@ -95,6 +95,9 @@ public class PageantStrategy implements SshIdentityAgentStrategy {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkComplete() {}
|
||||
|
||||
@Override
|
||||
public void buildCommand(CommandBuilder builder) {}
|
||||
|
||||
@@ -102,13 +105,13 @@ public class PageantStrategy implements SshIdentityAgentStrategy {
|
||||
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
|
||||
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
|
||||
var l = new ArrayList<>(List.of(
|
||||
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
|
||||
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
|
||||
new KeyValue("PKCS11Provider", "none")));
|
||||
KeyValue.raw("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
|
||||
KeyValue.escape("IdentityFile", file.isPresent() ? file.get() : "none"),
|
||||
KeyValue.raw("PKCS11Provider", "none")));
|
||||
|
||||
var agent = determinetAgentSocketLocation(sc);
|
||||
var agent = determineAgentSocketLocation(sc);
|
||||
if (agent != null) {
|
||||
l.add(new KeyValue("IdentityAgent", "\"" + agent + "\""));
|
||||
l.add(KeyValue.escape("IdentityAgent", agent));
|
||||
}
|
||||
|
||||
return l;
|
||||
|
||||
@@ -11,7 +11,6 @@ import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.pwman.PasswordManagerKeyConfiguration;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.FilePath;
|
||||
@@ -81,7 +80,8 @@ public class PasswordManagerAgentStrategy implements SshIdentityAgentStrategy {
|
||||
.hide(pwmanErrorProp.isNull())
|
||||
.nameAndDescription(useKeyName() ? "agentKeyName" : "publicKeyRequired")
|
||||
.documentationLink(DocumentationLink.SSH_AGENT_PUBLIC_KEYS)
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, identifier, useKeyName()), identifier)
|
||||
.addComp(
|
||||
new SshAgentKeyListComp(config.getFileSystem(), p, identifier, useKeyName(), false), identifier)
|
||||
.disable(pwmanErrorProp.isNotNull())
|
||||
.nonNull()
|
||||
.bind(
|
||||
@@ -126,7 +126,7 @@ public class PasswordManagerAgentStrategy implements SshIdentityAgentStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePath determinetAgentSocketLocation(ShellControl parent) {
|
||||
public FilePath determineAgentSocketLocation(ShellControl parent) {
|
||||
var config = getConfig();
|
||||
return config != null ? FilePath.of(config.getDefaultSocketLocation()) : null;
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.App;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.KeyValue;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonTypeName("passwordManagerInPlaceKey")
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
public class PasswordManagerInPlaceKeyStrategy implements SshIdentityAgentStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(
|
||||
Property<PasswordManagerInPlaceKeyStrategy> p, SshIdentityStrategyChoiceConfig config) {
|
||||
var options = new OptionsBuilder();
|
||||
var prefs = AppPrefs.get();
|
||||
var keyProperty = options.map(p, PasswordManagerInPlaceKeyStrategy::getKey);
|
||||
var field = new TextFieldComp(keyProperty).apply(struc -> struc.promptTextProperty()
|
||||
.bind(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return prefs.passwordManager().getValue() != null
|
||||
? prefs.passwordManager().getValue().getKeyPlaceholder()
|
||||
: "?";
|
||||
},
|
||||
prefs.passwordManager())));
|
||||
var button = new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
|
||||
AppPrefs.get().selectCategory("passwordManager");
|
||||
App.getApp().getStage().requestFocus();
|
||||
});
|
||||
var content = new InputGroupComp(List.of(field, button));
|
||||
content.setMainReference(field);
|
||||
return options.nameAndDescription("passwordManagerInPlaceKeyKey")
|
||||
.addComp(content, keyProperty)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
return PasswordManagerInPlaceKeyStrategy.builder()
|
||||
.key(keyProperty.get())
|
||||
.build();
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
String key;
|
||||
|
||||
@Override
|
||||
public void prepareParent(ShellControl parent) {}
|
||||
|
||||
@Override
|
||||
public void buildCommand(CommandBuilder builder) {}
|
||||
|
||||
@Override
|
||||
public List<KeyValue> configOptions(ShellControl sc) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKeyStrategy getPublicKeyStrategy() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePath determinetAgentSocketLocation(ShellControl parent) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
public interface SecurityKeyImpl {
|
||||
|
||||
static List<Class<?>> getClasses() {
|
||||
var l = new ArrayList<Class<?>>();
|
||||
l.add(OpenSc.class);
|
||||
l.add(YubikeyPiv.class);
|
||||
l.add(MacOsKeychain.class);
|
||||
l.add(Custom.class);
|
||||
return l;
|
||||
}
|
||||
|
||||
static List<Class<?>> getAvailable() {
|
||||
var l = new ArrayList<Class<?>>();
|
||||
l.add(OpenSc.class);
|
||||
l.add(YubikeyPiv.class);
|
||||
if (OsType.ofLocal() == OsType.MACOS) {
|
||||
l.add(MacOsKeychain.class);
|
||||
}
|
||||
l.add(Custom.class);
|
||||
return l;
|
||||
}
|
||||
|
||||
default boolean showLibraryPath() {
|
||||
return true;
|
||||
}
|
||||
|
||||
default void checkComplete() throws ValidationException {}
|
||||
|
||||
FilePath determineLibraryPath(ShellControl sc) throws Exception;
|
||||
|
||||
String getLink();
|
||||
|
||||
@JsonTypeName("yubikeyPiv")
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
class YubikeyPiv implements SecurityKeyImpl {
|
||||
|
||||
@Override
|
||||
public FilePath determineLibraryPath(ShellControl sc) throws Exception {
|
||||
var file =
|
||||
switch (sc.getOsType()) {
|
||||
case OsType.MacOs ignored -> FilePath.of("/usr/local/lib/libykcs11.dylib");
|
||||
case OsType.Windows ignored -> {
|
||||
var x64 = FilePath.of(
|
||||
sc.view().getEnvironmentVariableOrThrow("ProgramFiles"),
|
||||
"Yubico\\Yubico PIV Tool\\bin\\libykcs11.dll");
|
||||
if (sc.view().fileExists(x64)) {
|
||||
yield x64;
|
||||
}
|
||||
|
||||
var x86 = FilePath.of(
|
||||
sc.view().getEnvironmentVariableOrThrow("ProgramFiles(x86)"),
|
||||
"Yubico\\Yubico PIV Tool\\bin\\libykcs11.dll");
|
||||
if (sc.view().fileExists(x86)) {
|
||||
yield x86;
|
||||
}
|
||||
|
||||
yield x64;
|
||||
}
|
||||
default -> FilePath.of("/usr/local/lib/libykcs11.so");
|
||||
};
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLink() {
|
||||
return "https://developers.yubico.com/yubico-piv-tool/YKCS11/";
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("openSc")
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
class OpenSc implements SecurityKeyImpl {
|
||||
|
||||
@Override
|
||||
public String getLink() {
|
||||
return "https://github.com/opensc/opensc";
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePath determineLibraryPath(ShellControl sc) throws Exception {
|
||||
var file =
|
||||
switch (sc.getOsType()) {
|
||||
case OsType.MacOs ignored -> FilePath.of("/Library/OpenSC/lib/opensc-pkcs11.so");
|
||||
case OsType.Windows ignored -> {
|
||||
var x64 = FilePath.of(
|
||||
sc.view().getEnvironmentVariableOrThrow("ProgramFiles"),
|
||||
"OpenSC Project\\OpenSC\\pkcs11\\opensc-pkcs11.dll");
|
||||
if (sc.view().fileExists(x64)) {
|
||||
yield x64;
|
||||
}
|
||||
|
||||
var x86 = FilePath.of(
|
||||
sc.view().getEnvironmentVariableOrThrow("ProgramFiles(x86)"),
|
||||
"OpenSC Project\\OpenSC\\pkcs11\\opensc-pkcs11.dll");
|
||||
if (sc.view().fileExists(x86)) {
|
||||
yield x86;
|
||||
}
|
||||
|
||||
yield x64;
|
||||
}
|
||||
default -> FilePath.of("/usr/lib/pkcs11/opensc-pkcs11.so");
|
||||
};
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("macOsKeychain")
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
class MacOsKeychain implements SecurityKeyImpl {
|
||||
|
||||
@Override
|
||||
public String getLink() {
|
||||
return "https://support.apple.com/en-gb/guide/keychain-access/welcome/mac";
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePath determineLibraryPath(ShellControl sc) {
|
||||
var file =
|
||||
switch (sc.getOsType()) {
|
||||
case OsType.MacOs ignored -> FilePath.of("/usr/lib/ssh-keychain.dylib");
|
||||
default ->
|
||||
throw ErrorEventFactory.expected(
|
||||
new UnsupportedOperationException(
|
||||
"macOS keychain is not supported as a PKCS#11 provider on other operating systems"));
|
||||
};
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("customLibrary")
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
class Custom implements SecurityKeyImpl {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<Custom> p, SshIdentityStrategyChoiceConfig config) {
|
||||
var file = new SimpleObjectProperty<>(p.getValue().getFile());
|
||||
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("pkcs11Library")
|
||||
.addComp(
|
||||
new ContextualFileReferenceChoiceComp(
|
||||
config.getFileSystem() != null
|
||||
? config.getFileSystem()
|
||||
: new ReadOnlyObjectWrapper<>(
|
||||
DataStorage.get().local().ref()),
|
||||
file,
|
||||
null,
|
||||
List.of(),
|
||||
e -> {
|
||||
if (config.getFileSystem() == null) {
|
||||
return e.equals(DataStorage.get().local());
|
||||
}
|
||||
|
||||
var fs = config.getFileSystem().getValue();
|
||||
if (fs == null) {
|
||||
return e.equals(DataStorage.get().local());
|
||||
} else {
|
||||
return e.equals(fs.get());
|
||||
}
|
||||
},
|
||||
false),
|
||||
file)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
return new Custom(file.get());
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
FilePath file;
|
||||
|
||||
@Override
|
||||
public boolean showLibraryPath() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePath determineLibraryPath(ShellControl sc) {
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLink() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.core.AppInstallation;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.OptionsChoiceBuilder;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.KeyValue;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
@JsonTypeName("hardwareSecurityKey")
|
||||
@AllArgsConstructor
|
||||
public class SecurityKeyStrategy implements SshIdentityKeyListStrategy {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(
|
||||
Property<SecurityKeyStrategy> p, SshIdentityStrategyChoiceConfig config) {
|
||||
var publicKey = new SimpleStringProperty(p.getValue().getPublicKey());
|
||||
var securityKey = new SimpleObjectProperty<>(p.getValue().getSecurityKey());
|
||||
var filePath = new SimpleObjectProperty<String>();
|
||||
securityKey.subscribe(impl -> {
|
||||
if (impl == null) {
|
||||
filePath.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var fs = config.getFileSystem() != null && config.getFileSystem().getValue() != null
|
||||
? config.getFileSystem().getValue().getStore()
|
||||
: (ShellStore) DataStorage.get().local().getStore().asNeeded();
|
||||
var path = impl.determineLibraryPath(fs.getOrStartSession());
|
||||
filePath.set(path != null ? path.toString() : null);
|
||||
});
|
||||
});
|
||||
if (config.getFileSystem() != null) {
|
||||
config.getFileSystem().subscribe(fs -> {
|
||||
if (fs == null) {
|
||||
filePath.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var impl = securityKey.get();
|
||||
if (impl != null) {
|
||||
filePath.set(impl.determineLibraryPath(fs.getStore().getOrStartSession())
|
||||
.toString());
|
||||
} else {
|
||||
filePath.set(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var choice = OptionsChoiceBuilder.builder()
|
||||
.property(securityKey)
|
||||
.available(SecurityKeyImpl.getAvailable())
|
||||
.customConfiguration(config)
|
||||
.build()
|
||||
.build();
|
||||
|
||||
var showLibraryPath = Bindings.createBooleanBinding(() -> {
|
||||
if (filePath.get() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (securityKey.get() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return securityKey.get().showLibraryPath();
|
||||
}, filePath, securityKey);
|
||||
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("pkcs11Impl")
|
||||
.sub(choice, securityKey)
|
||||
.nonNull()
|
||||
.nameAndDescription("pkcs11Library")
|
||||
.addStaticString(filePath)
|
||||
.hide(Bindings.not(showLibraryPath))
|
||||
.nameAndDescription("publicKey")
|
||||
.documentationLink(DocumentationLink.SSH_AGENT_PUBLIC_KEYS)
|
||||
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false, true), publicKey)
|
||||
.bind(
|
||||
() -> {
|
||||
return SecurityKeyStrategy.builder()
|
||||
.securityKey(securityKey.get())
|
||||
.publicKey(publicKey.get())
|
||||
.build();
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
SecurityKeyImpl securityKey;
|
||||
String publicKey;
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(securityKey);
|
||||
securityKey.checkComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareParent(ShellControl parent) throws Exception {
|
||||
var file = securityKey.determineLibraryPath(parent);
|
||||
if (!parent.view().fileExists(file)) {
|
||||
var ex = new IOException("PKCS11 library at " + file + " not found");
|
||||
var event = ErrorEventFactory.fromThrowable(ex).expected();
|
||||
if (securityKey.getLink() != null) {
|
||||
event.link(securityKey.getLink());
|
||||
}
|
||||
ErrorEventFactory.preconfigure(event);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandBuilder createListCommand() {
|
||||
var cmd = CommandBuilder.of()
|
||||
.add("ssh-keygen", "-D")
|
||||
.addFile(sc -> securityKey.determineLibraryPath(sc).toUnix())
|
||||
.add("-e")
|
||||
.fixedEnvironment(
|
||||
"SSH_ASKPASS",
|
||||
AppInstallation.ofCurrent().getCliExecutablePath().toString())
|
||||
.fixedEnvironment("SSH_ASKPASS_REQUIRE", "force");
|
||||
ProcessControlProvider.get().addAskpassEnvironment(cmd, "[ssh-keygen]", null, null);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildCommand(CommandBuilder builder) {
|
||||
builder.setup(sc -> {
|
||||
var dir = securityKey.determineLibraryPath(sc).getParent();
|
||||
if (sc.getOsType() == OsType.WINDOWS) {
|
||||
builder.addToPath(dir, true);
|
||||
} else {
|
||||
builder.addToEnvironmentPath("LD_LIBRARY_PATH", dir, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
|
||||
var file = securityKey.determineLibraryPath(sc);
|
||||
var key = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
|
||||
return List.of(
|
||||
KeyValue.escape("PKCS11Provider", file.toString()),
|
||||
KeyValue.raw("IdentitiesOnly", key.isPresent() ? "yes" : "no"),
|
||||
KeyValue.escape("IdentityFile", key.isPresent() ? key.get().toString() : "none"),
|
||||
KeyValue.raw("IdentityAgent", "none"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKeyStrategy getPublicKeyStrategy() {
|
||||
return PublicKeyStrategy.Fixed.of(publicKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.comp.base.IntegratedTextAreaComp;
|
||||
import io.xpipe.app.comp.base.ModalButton;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.comp.base.TextAreaComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.LocalShell;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.process.ShellScript;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
public interface ShortLivedCertificateImpl extends Checkable {
|
||||
|
||||
static List<Class<?>> getClasses() {
|
||||
var l = new ArrayList<Class<?>>();
|
||||
l.add(HashicorpVault.class);
|
||||
l.add(OpenBao.class);
|
||||
l.add(Custom.class);
|
||||
return l;
|
||||
}
|
||||
|
||||
static void showDialogAndWait(FilePath privateKey, FilePath certificate, ShortLivedCertificateImpl impl)
|
||||
throws Exception {
|
||||
var summary = queryCertificateSummary(LocalShell.getShell(), certificate);
|
||||
var text = new TextAreaComp(new ReadOnlyObjectWrapper<>(summary));
|
||||
text.prefWidth(600);
|
||||
text.prefHeight(350);
|
||||
var modal = ModalOverlay.of(
|
||||
AppI18n.observable(
|
||||
!checkValid(summary) ? "certificateDialogExpiredTitle" : "certificateDialogTitle",
|
||||
certificate.getFileName()),
|
||||
text,
|
||||
null);
|
||||
var canRenew = impl != null && impl.supportsRenew();
|
||||
var renew = new SimpleBooleanProperty();
|
||||
if (canRenew) {
|
||||
modal.addButton(ModalButton.cancel());
|
||||
modal.addButton(new ModalButton(
|
||||
"renew",
|
||||
() -> {
|
||||
renew.set(true);
|
||||
},
|
||||
true,
|
||||
true));
|
||||
} else {
|
||||
modal.addButton(ModalButton.ok());
|
||||
}
|
||||
modal.showAndWait();
|
||||
|
||||
if (impl != null && renew.get()) {
|
||||
impl.renew(privateKey, certificate);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean checkValid(String text) {
|
||||
var matcher = Pattern.compile("Valid: from (\\S+) to (\\S+)").matcher(text);
|
||||
if (!matcher.find()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
var parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
|
||||
parser.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
var from = parser.parse(matcher.group(1)).toInstant();
|
||||
var to = parser.parse(matcher.group(2)).toInstant();
|
||||
return from.isBefore(Instant.now()) && to.isAfter(Instant.now());
|
||||
} catch (ParseException e) {
|
||||
ErrorEventFactory.fromThrowable(e).omit().handle();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static String queryCertificateSummary(ShellControl sc, FilePath f) throws Exception {
|
||||
var out = sc.command(
|
||||
CommandBuilder.of().add("ssh-keygen").add("-L", "-f").addFile(f))
|
||||
.readStdoutOrThrow();
|
||||
var minIndent = out.lines()
|
||||
.skip(1)
|
||||
.mapToInt(s -> {
|
||||
var m = Pattern.compile("^ *").matcher(s);
|
||||
return m.find() ? m.group().length() : 0;
|
||||
})
|
||||
.min()
|
||||
.orElse(0);
|
||||
var text = out.lines().skip(1).map(s -> s.substring(minIndent)).collect(Collectors.joining("\n"));
|
||||
return text;
|
||||
}
|
||||
|
||||
default boolean supportsRenew() {
|
||||
return true;
|
||||
}
|
||||
|
||||
default boolean supportsConfigure() {
|
||||
return true;
|
||||
}
|
||||
|
||||
default void checkComplete() throws ValidationException {}
|
||||
|
||||
void renew(FilePath privateKey, FilePath certificate) throws Exception;
|
||||
|
||||
void configure();
|
||||
|
||||
CacheableConfiguration<?> getCacheableConfiguration();
|
||||
|
||||
@JsonTypeName("openBao")
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
class OpenBao implements ShortLivedCertificateImpl {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<OpenBao> p) {
|
||||
var role = new SimpleStringProperty(p.getValue().getRole());
|
||||
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("certificateRole")
|
||||
.addString(role)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
return OpenBao.builder().role(role.get()).build();
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
String role;
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(role);
|
||||
OpenBaoConfig.get().get().checkComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renew(FilePath privateKey, FilePath certificate) throws Exception {
|
||||
OpenBaoConfig.get().get().renew(role, privateKey, certificate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure() {
|
||||
OpenBaoConfig.showDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheableConfiguration<?> getCacheableConfiguration() {
|
||||
return OpenBaoConfig.get();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("hashicorpVault")
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
class HashicorpVault implements ShortLivedCertificateImpl {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<HashicorpVault> p) {
|
||||
var role = new SimpleStringProperty(p.getValue().getRole());
|
||||
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("certificateRole")
|
||||
.addString(role)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
return HashicorpVault.builder().role(role.get()).build();
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
String role;
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(role);
|
||||
HashicorpVaultConfig.get().get().checkComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renew(FilePath privateKey, FilePath certificate) throws Exception {
|
||||
HashicorpVaultConfig.get().get().renew(role, privateKey, certificate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure() {
|
||||
HashicorpVaultConfig.showDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheableConfiguration<?> getCacheableConfiguration() {
|
||||
return HashicorpVaultConfig.get();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("custom")
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
class Custom implements ShortLivedCertificateImpl {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static OptionsBuilder createOptions(Property<Custom> p) {
|
||||
var command = new SimpleObjectProperty<>(p.getValue().getCommand());
|
||||
|
||||
return new OptionsBuilder()
|
||||
.nameAndDescription("certificateRenewCommand")
|
||||
.addComp(
|
||||
IntegratedTextAreaComp.script(
|
||||
new ReadOnlyObjectWrapper<>(
|
||||
DataStorage.get().local().ref()),
|
||||
command,
|
||||
true),
|
||||
command)
|
||||
.nonNull()
|
||||
.bind(
|
||||
() -> {
|
||||
return Custom.builder().command(command.get()).build();
|
||||
},
|
||||
p);
|
||||
}
|
||||
|
||||
ShellScript command;
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws ValidationException {
|
||||
Validators.nonNull(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renew(FilePath privateKey, FilePath certificate) throws Exception {
|
||||
var sc = LocalShell.get(Custom.class);
|
||||
sc.command(command.getValue()).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsConfigure() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure() {}
|
||||
|
||||
@Override
|
||||
public CacheableConfiguration<?> getCacheableConfiguration() {
|
||||
return HashicorpVaultConfig.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.LocalShell;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
|
||||
@@ -30,7 +29,8 @@ public class SshAgentKeyList {
|
||||
}
|
||||
|
||||
public static Entry findAgentIdentity(
|
||||
DataStoreEntryRef<ShellStore> ref, SshIdentityAgentStrategy strategy, String identifier) throws Exception {
|
||||
DataStoreEntryRef<ShellStore> ref, SshIdentityKeyListStrategy strategy, String identifier)
|
||||
throws Exception {
|
||||
var all = listAgentIdentities(ref, strategy);
|
||||
var list = all.stream()
|
||||
.filter(entry -> {
|
||||
@@ -73,16 +73,14 @@ public class SshAgentKeyList {
|
||||
return list.getFirst();
|
||||
}
|
||||
|
||||
public static List<Entry> listAgentIdentities(DataStoreEntryRef<ShellStore> ref, SshIdentityAgentStrategy strategy)
|
||||
throws Exception {
|
||||
public static List<Entry> listAgentIdentities(
|
||||
DataStoreEntryRef<ShellStore> ref, SshIdentityKeyListStrategy strategy) throws Exception {
|
||||
var session = ref != null ? ref.getStore().getOrStartSession() : LocalShell.getShell();
|
||||
strategy.prepareParent(session);
|
||||
|
||||
var socket = strategy.determinetAgentSocketLocation(session);
|
||||
var out = session.command(CommandBuilder.of()
|
||||
.add("ssh-add", "-L")
|
||||
.fixedEnvironment("SSH_AUTH_SOCK", socket != null ? socket.toString() : null))
|
||||
.readStdoutOrThrow();
|
||||
var cmd = strategy.createListCommand();
|
||||
strategy.buildCommand(cmd);
|
||||
var out = session.command(cmd).readStdoutOrThrow();
|
||||
var pattern = Pattern.compile("([^ ]+) ([^ ]+)\\s*(?: (.+))?");
|
||||
var lines = out.lines().toList();
|
||||
var list = new ArrayList<Entry>();
|
||||
|
||||
@@ -12,6 +12,7 @@ import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -29,19 +30,22 @@ import java.util.List;
|
||||
public class SshAgentKeyListComp extends SimpleRegionBuilder {
|
||||
|
||||
private final ObservableValue<DataStoreEntryRef<ShellStore>> ref;
|
||||
private final ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy;
|
||||
private final ObservableValue<? extends SshIdentityKeyListStrategy> sshIdentityStrategy;
|
||||
private final StringProperty value;
|
||||
private final boolean useKeyNames;
|
||||
private final boolean requireComplete;
|
||||
|
||||
public SshAgentKeyListComp(
|
||||
ObservableValue<DataStoreEntryRef<ShellStore>> ref,
|
||||
ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy,
|
||||
ObservableValue<? extends SshIdentityKeyListStrategy> sshIdentityStrategy,
|
||||
StringProperty value,
|
||||
boolean useKeyNames) {
|
||||
boolean useKeyNames,
|
||||
boolean requireComplete) {
|
||||
this.ref = ref;
|
||||
this.sshIdentityStrategy = sshIdentityStrategy;
|
||||
this.value = value;
|
||||
this.useKeyNames = useKeyNames;
|
||||
this.requireComplete = requireComplete;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,6 +54,18 @@ public class SshAgentKeyListComp extends SimpleRegionBuilder {
|
||||
field.apply(struc -> struc.setPromptText(
|
||||
useKeyNames ? "<name>" : "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== <key comment>"));
|
||||
var button = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2m-magnify-scan"), null);
|
||||
if (requireComplete) {
|
||||
button.disable(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
try {
|
||||
sshIdentityStrategy.getValue().checkComplete();
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
sshIdentityStrategy));
|
||||
}
|
||||
button.apply(struc -> {
|
||||
struc.setOnAction(event -> {
|
||||
DataStoreEntryRef<ShellStore> refToUse = ref != null && ref.getValue() != null
|
||||
|
||||
@@ -24,10 +24,10 @@ import atlantafx.base.theme.Styles;
|
||||
public class SshAgentTestComp extends SimpleRegionBuilder {
|
||||
|
||||
private final Runnable beforeTest;
|
||||
private final ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy;
|
||||
private final ObservableValue<? extends SshIdentityKeyListStrategy> sshIdentityStrategy;
|
||||
|
||||
public SshAgentTestComp(
|
||||
Runnable beforeTest, ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy) {
|
||||
Runnable beforeTest, ObservableValue<? extends SshIdentityKeyListStrategy> sshIdentityStrategy) {
|
||||
this.beforeTest = beforeTest;
|
||||
this.sshIdentityStrategy = sshIdentityStrategy;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
public interface SshIdentityAgentStrategy extends SshIdentityStrategy {
|
||||
public interface SshIdentityAgentStrategy extends SshIdentityKeyListStrategy {
|
||||
|
||||
void prepareParent(ShellControl parent) throws Exception;
|
||||
@Override
|
||||
default CommandBuilder createListCommand() {
|
||||
return CommandBuilder.of().add("ssh-add", "-L").environment("SSH_AUTH_SOCK", sc -> {
|
||||
var socket = determineAgentSocketLocation(sc);
|
||||
return socket != null ? socket.toString() : null;
|
||||
});
|
||||
}
|
||||
|
||||
FilePath determinetAgentSocketLocation(ShellControl parent) throws Exception;
|
||||
FilePath determineAgentSocketLocation(ShellControl parent) throws Exception;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.ext.ValidationException;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
|
||||
public interface SshIdentityKeyListStrategy extends SshIdentityStrategy {
|
||||
|
||||
void checkComplete() throws ValidationException;
|
||||
|
||||
void prepareParent(ShellControl parent) throws Exception;
|
||||
|
||||
CommandBuilder createListCommand();
|
||||
}
|
||||
@@ -52,7 +52,8 @@ public class SshIdentityStateManager {
|
||||
|
||||
if (gpg && gpgRunning) {
|
||||
// This sometimes takes a long time if the agent is not running. Why?
|
||||
sc.command(CommandBuilder.of().add("gpg-connect-agent", "killagent", "/bye")).executeAndCheck();
|
||||
sc.command(CommandBuilder.of().add("gpg-connect-agent", "killagent", "/bye"))
|
||||
.executeAndCheck();
|
||||
}
|
||||
|
||||
if (openssh && opensshRunning) {
|
||||
|
||||
@@ -27,6 +27,8 @@ public interface SshIdentityStrategy {
|
||||
l.add(KeyFileStrategy.class);
|
||||
l.add(OpenSshAgentStrategy.class);
|
||||
l.add(PasswordManagerAgentStrategy.class);
|
||||
l.add(CertificateKeyFileStrategy.class);
|
||||
l.add(SecurityKeyStrategy.class);
|
||||
if (OsType.ofLocal() != OsType.WINDOWS) {
|
||||
l.add(CustomAgentStrategy.class);
|
||||
}
|
||||
@@ -36,9 +38,7 @@ public interface SshIdentityStrategy {
|
||||
if (PageantStrategy.isSupported()) {
|
||||
l.add(PageantStrategy.class);
|
||||
}
|
||||
l.add(YubikeyPivStrategy.class);
|
||||
l.add(CustomPkcs11LibraryStrategy.class);
|
||||
l.add(OtherExternalAgentStrategy.class);
|
||||
l.add(OtherExternalIdentityStrategy.class);
|
||||
|
||||
return l;
|
||||
}
|
||||
@@ -50,17 +50,24 @@ public interface SshIdentityStrategy {
|
||||
l.add(KeyFileStrategy.class);
|
||||
l.add(OpenSshAgentStrategy.class);
|
||||
l.add(PasswordManagerAgentStrategy.class);
|
||||
l.add(PasswordManagerInPlaceKeyStrategy.class);
|
||||
l.add(CertificateKeyFileStrategy.class);
|
||||
l.add(SecurityKeyStrategy.class);
|
||||
l.add(CustomAgentStrategy.class);
|
||||
l.add(GpgAgentStrategy.class);
|
||||
l.add(PageantStrategy.class);
|
||||
l.add(YubikeyPivStrategy.class);
|
||||
l.add(CustomPkcs11LibraryStrategy.class);
|
||||
l.add(OtherExternalAgentStrategy.class);
|
||||
l.add(OtherExternalIdentityStrategy.class);
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
static FilePath getPublicKeyPath(FilePath file) {
|
||||
if (file.getExtension().isEmpty() || file.isDotFile()) {
|
||||
return FilePath.of(file + ".pub");
|
||||
} else {
|
||||
return FilePath.of(file.getBaseName() + ".pub");
|
||||
}
|
||||
}
|
||||
|
||||
static Optional<FilePath> getPublicKeyPath(ShellControl sc, String publicKey) throws Exception {
|
||||
if (publicKey == null || publicKey.isBlank()) {
|
||||
return Optional.empty();
|
||||
@@ -103,5 +110,9 @@ public interface SshIdentityStrategy {
|
||||
return new SecretNoneStrategy();
|
||||
}
|
||||
|
||||
default boolean supportsIdentityApply() {
|
||||
return true;
|
||||
}
|
||||
|
||||
PublicKeyStrategy getPublicKeyStrategy();
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package io.xpipe.app.cred;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.KeyValue;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
@Value
|
||||
@Jacksonized
|
||||
@Builder
|
||||
@JsonTypeName("yubikeyPiv")
|
||||
@AllArgsConstructor
|
||||
public class YubikeyPivStrategy implements SshIdentityStrategy {
|
||||
|
||||
private String getFile(ShellControl sc) {
|
||||
var file =
|
||||
switch (sc.getOsType()) {
|
||||
case OsType.MacOs ignored -> "/usr/local/lib/libykcs11.dylib";
|
||||
case OsType.Windows ignored -> {
|
||||
var x64 = "C:\\Program Files\\Yubico\\Yubico PIV Tool\\bin\\libykcs11.dll";
|
||||
if (Files.exists(Path.of(x64))) {
|
||||
yield x64;
|
||||
}
|
||||
|
||||
var x86 = "C:\\Program Files (x86)\\Yubico\\Yubico PIV Tool\\bin\\libykcs11.dll";
|
||||
if (Files.exists(Path.of(x86))) {
|
||||
yield x86;
|
||||
}
|
||||
|
||||
yield x64;
|
||||
}
|
||||
default -> "/usr/local/lib/libykcs11.so";
|
||||
};
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareParent(ShellControl parent) throws Exception {
|
||||
parent.requireLicensedFeature(LicenseProvider.get().getFeature("pkcs11Identity"));
|
||||
|
||||
var file = getFile(parent);
|
||||
if (!parent.getShellDialect().createFileExistsCommand(parent, file).executeAndCheck()) {
|
||||
throw ErrorEventFactory.expected(new IOException("Yubikey PKCS11 library at " + file + " not found"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildCommand(CommandBuilder builder) {
|
||||
builder.setup(sc -> {
|
||||
var file = getFile(sc);
|
||||
var dir = FilePath.of(file).getParent();
|
||||
if (sc.getOsType() == OsType.WINDOWS) {
|
||||
builder.addToPath(dir, true);
|
||||
} else {
|
||||
builder.addToEnvironmentPath("LD_LIBRARY_PATH", dir, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyValue> configOptions(ShellControl sc) {
|
||||
return List.of(
|
||||
new KeyValue("IdentitiesOnly", "no"),
|
||||
new KeyValue("PKCS11Provider", "\"" + getFile(sc) + "\""),
|
||||
new KeyValue("IdentityFile", "none"),
|
||||
new KeyValue("IdentityAgent", "none"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKeyStrategy getPublicKeyStrategy() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package io.xpipe.app.ext;
|
||||
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.process.ShellDialect;
|
||||
import io.xpipe.app.process.ShellDialects;
|
||||
import io.xpipe.app.process.ShellStoreState;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -16,10 +20,47 @@ import lombok.extern.jackson.Jacksonized;
|
||||
@Jacksonized
|
||||
public class ContainerStoreState extends ShellStoreState {
|
||||
|
||||
public static ShellDialect findSuitableDialect(ShellControl sc) throws Exception {
|
||||
if (!sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
|
||||
return sc.getOsType() == OsType.WINDOWS ? ShellDialects.CMD : ShellDialects.SH;
|
||||
}
|
||||
|
||||
if (sc.getOsType() != OsType.WINDOWS) {
|
||||
if (sc.view().findProgram("bash").isPresent()) {
|
||||
return ShellDialects.BASH;
|
||||
}
|
||||
|
||||
if (sc.view().findProgram("zsh").isPresent()) {
|
||||
return ShellDialects.ZSH;
|
||||
}
|
||||
|
||||
return ShellDialects.SH;
|
||||
} else {
|
||||
if (sc.view().findProgram("pwsh").isPresent()) {
|
||||
return ShellDialects.POWERSHELL_CORE;
|
||||
}
|
||||
|
||||
if (sc.view().findProgram("powershell").isPresent()) {
|
||||
return ShellDialects.POWERSHELL;
|
||||
}
|
||||
|
||||
return ShellDialects.CMD;
|
||||
}
|
||||
}
|
||||
|
||||
String imageName;
|
||||
String containerState;
|
||||
ShellDialect availableShellDialect;
|
||||
Boolean shellMissing;
|
||||
|
||||
public ShellDialect getEffectiveDialect(ShellControl sc) {
|
||||
if (availableShellDialect != null) {
|
||||
return availableShellDialect;
|
||||
}
|
||||
|
||||
return sc.getOsType() != OsType.WINDOWS ? ShellDialects.SH : ShellDialects.CMD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStoreState mergeCopy(DataStoreState newer) {
|
||||
var n = (ContainerStoreState) newer;
|
||||
@@ -32,6 +73,7 @@ public class ContainerStoreState extends ShellStoreState {
|
||||
super.mergeBuilder(css, b);
|
||||
b.containerState(useNewer(containerState, css.getContainerState()));
|
||||
b.imageName(useNewer(imageName, css.getImageName()));
|
||||
b.availableShellDialect(useNewer(availableShellDialect, css.getAvailableShellDialect()));
|
||||
b.shellMissing(useNewer(shellMissing, css.getShellMissing()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package io.xpipe.app.ext;
|
||||
|
||||
import io.xpipe.app.comp.BaseRegionBuilder;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.hub.comp.StoreSection;
|
||||
import io.xpipe.app.hub.comp.SystemStateComp;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -13,25 +15,48 @@ public interface CountGroupStoreProvider extends DataStoreProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
default BaseRegionBuilder<?, ?> stateDisplay(StoreSection section) {
|
||||
return new SystemStateComp(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
return section.getShownChildren().getList().isEmpty()
|
||||
? SystemStateComp.State.OTHER
|
||||
: SystemStateComp.State.SUCCESS;
|
||||
},
|
||||
section.getShownChildren().getList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
default ObservableValue<String> informationString(StoreSection section) {
|
||||
return Bindings.createStringBinding(
|
||||
() -> {
|
||||
var all = section.getAllChildren().getList();
|
||||
var allCount = all.stream()
|
||||
.filter(s -> !excludeNonCountable()
|
||||
|| s.getWrapper().getEntry().getProvider().includeInConnectionCount())
|
||||
.count();
|
||||
var shown = section.getShownChildren().getList();
|
||||
if (shown.size() == 0) {
|
||||
return AppI18n.get("noConnections");
|
||||
var shownCount = shown.stream()
|
||||
.filter(s -> !excludeNonCountable()
|
||||
|| s.getWrapper().getEntry().getProvider().includeInConnectionCount())
|
||||
.count();
|
||||
if (allCount == 0) {
|
||||
return AppI18n.get("no" + getCountTranslationKey() + "s");
|
||||
}
|
||||
|
||||
var string = all.size() == shown.size() ? all.size() : shown.size() + "/" + all.size();
|
||||
return all.size() > 0
|
||||
? (all.size() == 1
|
||||
? AppI18n.get("hasConnection", string)
|
||||
: AppI18n.get("hasConnections", string))
|
||||
: AppI18n.get("noConnections");
|
||||
var string = allCount == shownCount ? allCount : shownCount + "/" + allCount;
|
||||
return allCount == 1
|
||||
? AppI18n.get("has" + getCountTranslationKey(), string)
|
||||
: AppI18n.get("has" + getCountTranslationKey() + "s", string);
|
||||
},
|
||||
section.getShownChildren().getList(),
|
||||
section.getAllChildren().getList(),
|
||||
AppI18n.activeLanguage());
|
||||
}
|
||||
|
||||
String getCountTranslationKey();
|
||||
|
||||
default boolean excludeNonCountable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,21 @@ import java.util.UUID;
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum DataStoreCreationCategory {
|
||||
HOST(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),
|
||||
SHELL(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),
|
||||
COMMAND(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),
|
||||
TUNNEL(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),
|
||||
SERVICE(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),
|
||||
SCRIPT(DataStorage.ALL_SCRIPTS_CATEGORY_UUID),
|
||||
CLUSTER(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),
|
||||
DESKTOP(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),
|
||||
SERIAL(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),
|
||||
MACRO(DataStorage.ALL_MACROS_CATEGORY_UUID),
|
||||
FILE_SYSTEM(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID),
|
||||
IDENTITY(DataStorage.ALL_IDENTITIES_CATEGORY_UUID);
|
||||
HOST(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID, "ssh"),
|
||||
SHELL(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID, null),
|
||||
COMMAND(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID, null),
|
||||
TUNNEL(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID, "sshLocalTunnel"),
|
||||
SERVICE(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID, "customService"),
|
||||
SCRIPT(DataStorage.ALL_SCRIPTS_CATEGORY_UUID, "script"),
|
||||
SCRIPT_SOURCE(DataStorage.ALL_SCRIPTS_CATEGORY_UUID, "scriptCollectionSource"),
|
||||
CLUSTER(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID, null),
|
||||
DESKTOP(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID, null),
|
||||
SERIAL(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID, null),
|
||||
MACRO(DataStorage.ALL_MACROS_CATEGORY_UUID, null),
|
||||
FILE_SYSTEM(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID, null),
|
||||
IDENTITY(DataStorage.ALL_IDENTITIES_CATEGORY_UUID, "localIdentity"),
|
||||
NETWORK(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID, null);
|
||||
|
||||
private final UUID category;
|
||||
private final String defaultProvider;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.xpipe.app.comp.BaseRegionBuilder;
|
||||
import io.xpipe.app.comp.RegionBuilder;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppImages;
|
||||
import io.xpipe.app.hub.comp.StoreCreationModel;
|
||||
import io.xpipe.app.hub.comp.StoreEntryComp;
|
||||
import io.xpipe.app.hub.comp.StoreEntryWrapper;
|
||||
import io.xpipe.app.hub.comp.StoreSection;
|
||||
@@ -115,7 +116,7 @@ public interface DataStoreProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
default BaseRegionBuilder<?, ?> stateDisplay(StoreEntryWrapper w) {
|
||||
default BaseRegionBuilder<?, ?> stateDisplay(StoreSection section) {
|
||||
return RegionBuilder.empty();
|
||||
}
|
||||
|
||||
@@ -137,7 +138,7 @@ public interface DataStoreProvider {
|
||||
return DataStoreUsageCategory.COMMAND;
|
||||
}
|
||||
|
||||
if (cc == DataStoreCreationCategory.SCRIPT) {
|
||||
if (cc == DataStoreCreationCategory.SCRIPT || cc == DataStoreCreationCategory.SCRIPT_SOURCE) {
|
||||
return DataStoreUsageCategory.SCRIPT;
|
||||
}
|
||||
|
||||
@@ -160,7 +161,7 @@ public interface DataStoreProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
default GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
|
||||
default GuiDialog guiDialog(StoreCreationModel model, Property<DataStore> store) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package io.xpipe.app.ext;
|
||||
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package io.xpipe.app.ext;
|
||||
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.browser.BrowserStoreSessionTab;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.CommandControl;
|
||||
@@ -10,14 +7,20 @@ import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.process.ShellDialect;
|
||||
import io.xpipe.app.secret.SecretRetrievalStrategy;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.HttpProxy;
|
||||
import io.xpipe.app.util.RemoteDesktopDockContentEntry;
|
||||
import io.xpipe.app.vnc.VncBaseStore;
|
||||
import io.xpipe.core.SecretValue;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class ProcessControlProvider {
|
||||
|
||||
@@ -40,8 +43,8 @@ public abstract class ProcessControlProvider {
|
||||
|
||||
public abstract ShellStore subShellEnvironment(DataStoreEntryRef<ShellStore> s, ShellDialect dialect);
|
||||
|
||||
public abstract BrowserStoreSessionTab<?> createVncSession(
|
||||
BrowserFullSessionModel model, DataStoreEntryRef<VncBaseStore> ref);
|
||||
public abstract RemoteDesktopDockContentEntry createVncSession(
|
||||
DataStoreEntryRef<VncBaseStore> ref, Runnable onKill);
|
||||
|
||||
public abstract DataStoreEntryRef<ShellStore> elevated(DataStoreEntryRef<ShellStore> e);
|
||||
|
||||
@@ -79,4 +82,11 @@ public abstract class ProcessControlProvider {
|
||||
public abstract void cloneRepository(String url, Path target) throws Exception;
|
||||
|
||||
public abstract void pullRepository(Path target) throws Exception;
|
||||
|
||||
public abstract Optional<HttpProxy> getHttpProxy(DataStoreEntryRef<?> store);
|
||||
|
||||
public abstract void addAskpassEnvironment(
|
||||
CommandBuilder b, String prefix, UUID requestId, UUID secretId, String... askpassName);
|
||||
|
||||
public abstract void refreshWsl();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ public abstract class Session implements AutoCloseable {
|
||||
if (!checkAliveQuiet()) {
|
||||
handleSessionDeath();
|
||||
}
|
||||
|
||||
if (checkInactive()) {
|
||||
handleSessionDeath();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
@@ -60,6 +64,8 @@ public abstract class Session implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
public abstract boolean checkInactive();
|
||||
|
||||
public abstract boolean checkAlive() throws Exception;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package io.xpipe.app.ext;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.core.FailableSupplier;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Getter
|
||||
public class ShellSession extends Session {
|
||||
|
||||
@@ -84,6 +87,22 @@ public class ShellSession extends Session {
|
||||
shellControl.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkInactive() {
|
||||
var secs = AppPrefs.get() != null
|
||||
? AppPrefs.get().backgroundSessionInactivityTimeout().getValue()
|
||||
: null;
|
||||
if (secs == null || secs <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (secs < 30) {
|
||||
secs = 30;
|
||||
}
|
||||
|
||||
return shellControl.isInactive(Duration.ofSeconds(secs));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkAlive() throws Exception {
|
||||
if (shellControl == null) {
|
||||
@@ -104,11 +123,16 @@ public class ShellSession extends Session {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!shellControl.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Don't print it constantly
|
||||
return shellControl
|
||||
.command(CommandBuilder.of().add("echo", "xpipetest"))
|
||||
.sensitive()
|
||||
.noActivity()
|
||||
.executeAndCheck();
|
||||
} catch (Exception ex) {
|
||||
throw ErrorEventFactory.expected(ex);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user