This commit is contained in:
crschnick committed 2026-09-20 17:14:59 +00:00
1 parent 93cbb01b13
commit f3952dabcc
11 files changed
+338 -6

No files matched your search

@@ -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;
@@ -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<ElevatedExecCallbackExchange.Request> {
@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 {}
}
@@ -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<ElevatedExecQueryExchange.Request> {
@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;
}
}
@@ -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);
}
@@ -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);
@@ -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);
@@ -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;
}
@@ -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();
@@ -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<ElevatedExec> 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<ElevatedExec> getNextElevatedExec() {
while (!AppOperationMode.isInShutdown()) {
Optional<ElevatedExec> 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<ElevatedExec> 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;
}
}
@@ -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();
}
}
}
+2
View File
@@ -257,6 +257,8 @@ open module io.xpipe.app {
TerminalLaunchExchange,
TerminalExternalLaunchExchange,
SshLaunchExchange,
ElevatedExecQueryExchange,
ElevatedExecCallbackExchange,
FsReadExchange,
FsBlobExchange,
FsWriteExchange,