From f3952dabcce5db8a9b70cb18feef8b8faf125c70 Mon Sep 17 00:00:00 2001 From: crschnick Date: Sun, 20 Sep 2026 17:14:59 +0000 Subject: [PATCH] Rework --- .../xpipe/app/action/LauncherUrlProvider.java | 4 + .../api/ElevatedExecCallbackExchange.java | 62 +++++++++ .../beacon/api/ElevatedExecQueryExchange.java | 57 ++++++++ .../io/xpipe/app/core/AppOpenArguments.java | 10 +- .../java/io/xpipe/app/core/AppProperties.java | 4 + .../xpipe/app/core/mode/AppOperationMode.java | 7 + .../io/xpipe/app/ext/ProcModuleProvider.java | 2 + .../java/io/xpipe/app/process/LocalShell.java | 16 +++ .../java/io/xpipe/app/util/ElevatedExec.java | 131 ++++++++++++++++++ .../io/xpipe/app/util/ElevatedExecClient.java | 49 +++++++ app/src/main/java/module-info.java | 2 + 11 files changed, 338 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/beacon/api/ElevatedExecCallbackExchange.java create mode 100644 app/src/main/java/io/xpipe/app/beacon/api/ElevatedExecQueryExchange.java create mode 100644 app/src/main/java/io/xpipe/app/util/ElevatedExec.java create mode 100644 app/src/main/java/io/xpipe/app/util/ElevatedExecClient.java diff --git a/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java b/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java index cf6fc9dd0..a20761834 100644 --- a/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java +++ b/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java @@ -13,6 +13,10 @@ public interface LauncherUrlProvider extends ActionProvider { .map(lup -> (LauncherUrlProvider) lup); } + default boolean executeInPlace() { + return false; + } + String getScheme(); AbstractAction createAction(URI uri) throws Exception; diff --git a/app/src/main/java/io/xpipe/app/beacon/api/ElevatedExecCallbackExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/ElevatedExecCallbackExchange.java new file mode 100644 index 000000000..5dd28d77e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/ElevatedExecCallbackExchange.java @@ -0,0 +1,62 @@ +package io.xpipe.app.beacon.api; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.util.ElevatedExec; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import java.util.UUID; + +public class ElevatedExecCallbackExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/exec/elevated-callback"; + } + + @Override + public boolean requiresCompletedStartup() { + return false; + } + + @Override + public Object handle(HttpExchange exchange, Request msg) throws Exception { + var found = ElevatedExec.getElevatedExec(msg.getId()); + if (found.isEmpty()) { + throw new BeaconClientException("No elevated exec found for " + msg.getId()); + } else { + ElevatedExec.completeElevatedExec(msg.getId(), msg.getStdout(), msg.getStderr(), msg.getExitCode()); + return Response.builder().build(); + } + } + + @Override + public boolean requiresEnabledApi() { + return false; + } + + @Jacksonized + @Builder + @Value + public static class Request { + + @NonNull + UUID id; + + @NonNull + String stdout; + + @NonNull + String stderr; + + long exitCode; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/app/src/main/java/io/xpipe/app/beacon/api/ElevatedExecQueryExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/ElevatedExecQueryExchange.java new file mode 100644 index 000000000..0b71f098f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/ElevatedExecQueryExchange.java @@ -0,0 +1,57 @@ +package io.xpipe.app.beacon.api; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.process.ShellDialect; +import io.xpipe.app.process.ShellScript; +import io.xpipe.app.util.ElevatedExec; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class ElevatedExecQueryExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/exec/elevated-query"; + } + + @Override + public boolean requiresCompletedStartup() { + return false; + } + + @Override + public Object handle(HttpExchange exchange, Request msg) throws Exception { + var found = ElevatedExec.getNextElevatedExec(); + if (found.isEmpty()) { + return Response.builder().build(); + } else { + return Response.builder().id(found.get().getId()).dialect(found.get().getDialect()).command(found.get().getCommand()).build(); + } + } + + @Override + public boolean requiresEnabledApi() { + return false; + } + + @Jacksonized + @Builder + @Value + public static class Request {} + + @Jacksonized + @Builder + @Value + public static class Response { + + UUID id; + + ShellDialect dialect; + + ShellScript command; + } +} diff --git a/app/src/main/java/io/xpipe/app/core/AppOpenArguments.java b/app/src/main/java/io/xpipe/app/core/AppOpenArguments.java index fa9dea45e..657b99787 100644 --- a/app/src/main/java/io/xpipe/app/core/AppOpenArguments.java +++ b/app/src/main/java/io/xpipe/app/core/AppOpenArguments.java @@ -56,12 +56,6 @@ public class AppOpenArguments { } }); - // var requiresPlatform = all.stream().anyMatch(launcherInput -> launcherInput.requiresJavaFXPlatform()); - // if (requiresPlatform) { - // OperationMode.switchToSyncIfPossible(OperationMode.GUI); - // } - // var hasGui = OperationMode.get() == OperationMode.GUI; - all.forEach(launcherInput -> { launcherInput.executeAsync(); }); @@ -72,6 +66,10 @@ public class AppOpenArguments { return List.of(); } + if (input.startsWith("'") && input.endsWith("'")) { + input = input.substring(1, input.length() - 1); + } + if (input.startsWith("\"") && input.endsWith("\"")) { input = input.substring(1, input.length() - 1); } diff --git a/app/src/main/java/io/xpipe/app/core/AppProperties.java b/app/src/main/java/io/xpipe/app/core/AppProperties.java index dc197679e..cf441982f 100644 --- a/app/src/main/java/io/xpipe/app/core/AppProperties.java +++ b/app/src/main/java/io/xpipe/app/core/AppProperties.java @@ -69,6 +69,7 @@ public class AppProperties { boolean isDaemon; boolean isCli; boolean printBeaconMessages; + boolean elevatedExecMode; @NonFinal @Getter(AccessLevel.PRIVATE) @@ -131,6 +132,9 @@ public class AppProperties { printBeaconMessages = Optional.ofNullable(System.getProperty(AppNames.propertyName("printBeaconMessages"))) .map(Boolean::parseBoolean) .orElse(false); + elevatedExecMode = Optional.ofNullable(System.getProperty(AppNames.propertyName("elevatedExecMode"))) + .map(Boolean::parseBoolean) + .orElse(false); debugThreads = Optional.ofNullable(System.getProperty(AppNames.propertyName("debugThreads"))) .map(Boolean::parseBoolean) .orElse(false); diff --git a/app/src/main/java/io/xpipe/app/core/mode/AppOperationMode.java b/app/src/main/java/io/xpipe/app/core/mode/AppOperationMode.java index 5c2f3b9d9..c65266734 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/AppOperationMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/AppOperationMode.java @@ -196,6 +196,13 @@ public abstract class AppOperationMode { AppExtensionManager.init(); AppI18n.init(); AppPrefs.initLocal(); + + if (AppProperties.get().isElevatedExecMode()) { + LocalShell.init(); + ElevatedExecClient.runLoop(); + return; + } + AppInstance.init(); // Initialize early to load in parallel PlatformInit.init(false); diff --git a/app/src/main/java/io/xpipe/app/ext/ProcModuleProvider.java b/app/src/main/java/io/xpipe/app/ext/ProcModuleProvider.java index 109a801c5..3ef66dd94 100644 --- a/app/src/main/java/io/xpipe/app/ext/ProcModuleProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/ProcModuleProvider.java @@ -115,4 +115,6 @@ public abstract class ProcModuleProvider { public abstract void importRdpFile(Path file) throws Exception; public abstract BaseRegionBuilder createCustomSshAgentTest(); + + public abstract void runElevatedCommand(CommandBuilder b) throws Exception; } diff --git a/app/src/main/java/io/xpipe/app/process/LocalShell.java b/app/src/main/java/io/xpipe/app/process/LocalShell.java index 6c44ee109..c41ad5c81 100644 --- a/app/src/main/java/io/xpipe/app/process/LocalShell.java +++ b/app/src/main/java/io/xpipe/app/process/LocalShell.java @@ -55,6 +55,22 @@ public class LocalShell { return local != null; } + public static ShellControl getLocalShell(ShellDialect sc) { + if (sc == getDialect()) { + return local; + } + + if (ShellDialects.isPowershell(sc)) { + var pwsh = getLocalPowershell(); + if (pwsh.isEmpty()) { + throw new IllegalStateException("Unable to run " + sc + " commands on this system"); + } + return pwsh.get(); + } else { + throw new IllegalStateException("Unable to run " + sc + " commands on this system"); + } + } + public static synchronized ShellControl init() throws Exception { if (local == null) { local = ProcModuleProvider.get().createLocalProcessControl(false).start(); diff --git a/app/src/main/java/io/xpipe/app/util/ElevatedExec.java b/app/src/main/java/io/xpipe/app/util/ElevatedExec.java new file mode 100644 index 000000000..1eccac9e2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/ElevatedExec.java @@ -0,0 +1,131 @@ +package io.xpipe.app.util; + +import io.xpipe.app.core.AppInstallation; +import io.xpipe.app.core.AppOpenArguments; +import io.xpipe.app.core.AppProperties; +import io.xpipe.app.core.mode.AppOperationMode; +import io.xpipe.app.ext.ProcModuleProvider; +import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.process.*; +import lombok.Getter; + +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Getter +public class ElevatedExec { + + private static final List execs = new ArrayList<>(); + private static boolean daemonRunning = false; + + private static synchronized void startElevatedDaemonIfNeeded() throws Exception { + if (daemonRunning) { + return; + } + + var cmd = AppInstallation.ofCurrent().getDaemonExecutablePath(); + ProcModuleProvider.get().runElevatedCommand(CommandBuilder.of().addFile(cmd) + .add("-Dio.xpipe.app.dataDir=\"" + AppProperties.get().getDataDir() + "\"") + .add("-Dio.xpipe.app.elevatedExecMode=true")); + + var exec = scheduleElevated(CommandBuilder.of().add(LocalShell.getDialect().getPrintWorkingDirectoryCommand())); + var success = exec.getLatch().await(30, TimeUnit.SECONDS); + if (!success) { + throw ErrorEventFactory.expected(new IllegalStateException("Elevated daemon did not start up in time")); + } + daemonRunning = true; + } + + public static ElevatedExec runElevated(CommandBuilder b) throws Exception { + startElevatedDaemonIfNeeded(); + + var exec = scheduleElevated(b); + for (int i = 0; i < 30; i++) { + if (exec.isFinished()) { + return exec; + } + + Thread.sleep(1000); + } + throw ErrorEventFactory.expected(new IllegalStateException("Elevated command did not complete in time")); + } + + private static ElevatedExec scheduleElevated(CommandBuilder b) throws Exception { + var full = b.buildFull(LocalShell.getShell()); + var id = UUID.randomUUID(); + var exec = new ElevatedExec(id, LocalShell.getDialect(), ShellScript.of(full)); + synchronized (execs) { + execs.add(exec); + } + return exec; + } + + public static void completeElevatedExec(UUID id, String stdout, String stderr, long exitCode) { + var e = getElevatedExec(id); + if (e.isEmpty()) { + return; + } + + e.get().stdout = stdout; + e.get().stderr = stderr; + e.get().exitCode = exitCode; + e.get().latch.countDown(); + } + + public static Optional getNextElevatedExec() { + while (!AppOperationMode.isInShutdown()) { + Optional e; + synchronized (execs) { + e = execs.stream().filter(x -> x.isScheduled()).findFirst(); + } + if (e.isPresent()) { + e.get().getLatch().countDown(); + return e; + } else { + ThreadHelper.sleep(100); + } + } + return Optional.empty(); + } + + public static Optional getElevatedExec(UUID id) { + synchronized (execs) { + var e = execs.stream().filter(x -> x.getId().equals(id)).findFirst(); + return e; + } + } + + public ElevatedExec(UUID id, ShellDialect dialect, ShellScript command) { + this.id = id; + this.dialect = dialect; + this.command = command; + } + + private final UUID id; + private final ShellDialect dialect; + private final ShellScript command; + private final CountDownLatch latch = new CountDownLatch(2); + + private String stdout; + private String stderr; + private long exitCode; + + public void throwIfFailed() throws ProcessOutputException { + if (exitCode != 0) { + throw ProcessOutputException.of(exitCode, stdout, stderr); + } + } + + private boolean isScheduled() { + return latch.getCount() == 2; + } + + private boolean isActive() { + return latch.getCount() == 1; + } + + private boolean isFinished() { + return latch.getCount() == 0; + } +} diff --git a/app/src/main/java/io/xpipe/app/util/ElevatedExecClient.java b/app/src/main/java/io/xpipe/app/util/ElevatedExecClient.java new file mode 100644 index 000000000..2aeff7e6f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/ElevatedExecClient.java @@ -0,0 +1,49 @@ +package io.xpipe.app.util; + +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.beacon.BeaconClient; +import io.xpipe.app.beacon.BeaconClientInformation; +import io.xpipe.app.beacon.api.ElevatedExecCallbackExchange; +import io.xpipe.app.beacon.api.ElevatedExecQueryExchange; +import io.xpipe.app.core.AppProperties; +import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.process.LocalShell; + +public class ElevatedExecClient { + + public static void runLoop() { + if (OsType.ofLocal() != OsType.WINDOWS) { + return; + } + + if (AppBeaconServer.get() != null) { + return; + } + + try { + var port = AppProperties.get().queryEffectiveBeaconPort(true).orElse(AppProperties.get().getDefaultBeaconPort()); + var client = BeaconClient.establishConnection(port, BeaconClientInformation.Daemon.builder().build(), + AppProperties.get().getBeaconAuthFile()); + + while (true) { + ElevatedExecQueryExchange.Response response = client.performRequest(ElevatedExecQueryExchange.Request.builder().build()); + if (response.getId() == null) { + break; + } + + var sc = LocalShell.getLocalShell(response.getDialect()); + var command = sc.command(response.getCommand()); + + var out = command.readStdoutAndStderr(); + client.performRequest(ElevatedExecCallbackExchange.Request.builder() + .id(response.getId()) + .stdout(out[0]) + .stderr(out[1]) + .exitCode(command.getExitCode()) + .build()); + } + } catch (Throwable e) { + ErrorEventFactory.fromThrowable(e).expected().omit().term().handle(); + } + } +} diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index 9a7321447..26940b614 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -257,6 +257,8 @@ open module io.xpipe.app { TerminalLaunchExchange, TerminalExternalLaunchExchange, SshLaunchExchange, + ElevatedExecQueryExchange, + ElevatedExecCallbackExchange, FsReadExchange, FsBlobExchange, FsWriteExchange,