This commit is contained in:
crschnick
2026-05-24 18:15:19 +00:00
parent 7b1d0e574f
commit 56b7d2f773
18 changed files with 217 additions and 205 deletions
+4
View File
@@ -123,6 +123,10 @@ run {
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList())
classpath += exts
def cli = project(':cli').getTasksByName('jar', true)[0].outputs.files.singleFile
classpath += files(cli)
dependsOn(project(':cli').getTasksByName('jar', true)[0])
dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList())
}
+2 -8
View File
@@ -13,14 +13,8 @@ public class Main {
return;
}
// Since this is not marked as a console application, it will not print anything when you run it in a console on
// Windows
if (args.length == 1 && args[0].equals("--help")) {
System.out.printf("""
The daemon executable %s does not accept any command-line arguments.
For a reference on how to use xpipe from the command-line, take a look at https://docs.xpipe.io/cli.
%n""", AppNames.ofCurrent().getExecutableName());
if (args.length == 1 && (args[0].equals("--help") || args[0].equals("help"))) {
System.out.println("For a reference on how to use xpipe from the command-line, take a look at https://docs.xpipe.io/cli");
return;
}
@@ -2,11 +2,11 @@ package io.xpipe.app.beacon;
import io.xpipe.app.beacon.mcp.AppMcpServer;
import io.xpipe.app.core.AppLocalTemp;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.beacon.BeaconConfig;
import io.xpipe.beacon.BeaconInterface;
import io.xpipe.core.OsType;
@@ -56,7 +56,8 @@ public class AppBeaconServer {
public static void init() {
try {
INSTANCE = new AppBeaconServer(BeaconConfig.getUsedPort());
// We already queried the beacon port at this point, so this will always work
INSTANCE = new AppBeaconServer(AppProperties.get().queryEffectiveBeaconPort(false).orElse(AppProperties.get().getDefaultBeaconPort()));
INSTANCE.initAuthSecret();
INSTANCE.start();
TrackEvent.withInfo("Started http server")
@@ -111,7 +112,7 @@ public class AppBeaconServer {
}
private void initAuthSecret() throws IOException {
var file = BeaconConfig.getLocalBeaconAuthFile();
var file = AppProperties.get().getBeaconAuthFile();
// Create and set temp dir permissions for Linux
AppLocalTemp.getLocalTempDataDirectory();
@@ -122,13 +123,13 @@ public class AppBeaconServer {
}
localAuthSecret = id;
var lockFile = BeaconConfig.getLocalBeaconLockFile();
var lockFile = AppProperties.get().getBeaconLockFile();
localLockFileChannel = new RandomAccessFile(lockFile.toFile(), "rw").getChannel();
localLockFileLock = localLockFileChannel.tryLock();
}
private void deleteAuthSecret() {
var file = BeaconConfig.getLocalBeaconAuthFile();
var file = AppProperties.get().getBeaconAuthFile();
try {
Files.delete(file);
localLockFileLock.release();
@@ -185,13 +185,13 @@ public abstract class AppInstallation {
}
@Override
public Path getLangPath() {
return getBaseInstallationPath().resolve("lang");
public Path getCliExecutablePath() {
return getBaseInstallationPath().resolve("xpipe.exe");
}
@Override
public Path getCliExecutablePath() {
return getBaseInstallationPath().resolve("bin", "xpipe.exe");
public Path getLangPath() {
return getBaseInstallationPath().resolve("lang");
}
@Override
@@ -243,13 +243,13 @@ public abstract class AppInstallation {
}
@Override
public Path getLangPath() {
return getBaseInstallationPath().resolve("lang");
public Path getCliExecutablePath() {
return getBaseInstallationPath().resolve("bin", "xpipe");
}
@Override
public Path getCliExecutablePath() {
return getBaseInstallationPath().resolve("bin", "xpipe");
public Path getLangPath() {
return getBaseInstallationPath().resolve("lang");
}
@Override
@@ -303,6 +303,11 @@ public abstract class AppInstallation {
AppNames.ofCurrent().getExecutableName() + "_debug.sh");
}
@Override
public Path getCliExecutablePath() {
return getBaseInstallationPath().resolve("Contents", "MacOS", "xpipe");
}
@Override
public Path getLangPath() {
if (!AppProperties.get().isImage()) {
@@ -312,11 +317,6 @@ public abstract class AppInstallation {
return getBaseInstallationPath().resolve("Contents", "Resources", "lang");
}
@Override
public Path getCliExecutablePath() {
return getBaseInstallationPath().resolve("Contents", "MacOS", "xpipe");
}
@Override
public Path getDaemonExecutablePath() {
return getBaseInstallationPath()
@@ -1,6 +1,5 @@
package io.xpipe.app.core;
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;
@@ -8,16 +7,12 @@ 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;
@@ -30,7 +25,7 @@ public class AppInstance {
public static Optional<BeaconClient> tryEstablishConnection(int port) {
try {
return Optional.of(BeaconClient.establishConnection(
port, BeaconClientInformation.Daemon.builder().build()));
port, BeaconClientInformation.Daemon.builder().build(), AppProperties.get().getBeaconAuthFile()));
} catch (Exception ex) {
ErrorEventFactory.fromThrowable(ex).omit().expected().handle();
return Optional.empty();
@@ -38,40 +33,17 @@ public class AppInstance {
}
private static void checkStart(int attemptCounter) {
var port = BeaconConfig.getUsedPort();
var port = AppProperties.get().getDefaultBeaconPort();
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;
}
}
var effectiveBeaconPort = AppProperties.get().queryEffectiveBeaconPort(reachable);
if (effectiveBeaconPort.isEmpty()) {
ErrorEventFactory.fromMessage("Unable to find free beacon port")
.term()
.documentationLink(DocumentationLink.BEACON_PORT_BIND)
.expected()
.handle();
AppOperationMode.halt(1);
}
if (!reachable) {
@@ -3,11 +3,17 @@ package io.xpipe.app.core;
import io.xpipe.app.core.check.AppDirectoryPermissionsCheck;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.beacon.BeaconServer;
import io.xpipe.core.OsType;
import io.xpipe.core.XPipeDaemonMode;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Value;
import lombok.experimental.NonFinal;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
@@ -56,6 +62,15 @@ public class AppProperties {
boolean logPlatformDebug;
String logLevel;
String loginTarget;
int defaultBeaconPort;
Path beaconAuthFile;
Path beaconLockFile;
boolean debugCli;
boolean isDaemon;
boolean isCli;
@NonFinal
@Getter(AccessLevel.PRIVATE)
Integer effectiveBeaconPort;
public AppProperties(String[] args) {
var appDir = Path.of(System.getProperty("user.dir")).resolve("app");
@@ -158,6 +173,12 @@ public class AppProperties {
.orElse("info");
loginTarget = Optional.ofNullable(System.getProperty(AppNames.propertyName("login")))
.orElse(null);
isCli = Optional.ofNullable(System.getProperty(AppNames.propertyName("isCli")))
.map(Boolean::parseBoolean)
.orElse(false);
isDaemon = Optional.ofNullable(System.getProperty(AppNames.propertyName("isDaemon")))
.map(Boolean::parseBoolean)
.orElse(!isCli);
// We require the user dir from here
AppDirectoryPermissionsCheck.checkDirectory(dataDir);
@@ -180,6 +201,93 @@ public class AppProperties {
.orElse(false);
explicitMode = XPipeDaemonMode.getIfPresent(System.getProperty(AppNames.propertyName("mode")))
.orElse(null);
defaultBeaconPort = getDefaultBeaconPort(staging);
beaconAuthFile = getLocalBeaconAuthFile(staging);
beaconLockFile = beaconAuthFile.getParent().resolve("lock");
clearLeftoverAuthFile();
debugCli = Optional.ofNullable(System.getProperty(AppNames.propertyName("debugCli")))
.map(Boolean::parseBoolean)
.orElse(false);
}
public OptionalInt queryEffectiveBeaconPort(boolean reachable) {
if (!reachable) {
effectiveBeaconPort = getDefaultBeaconPort();
return OptionalInt.of(effectiveBeaconPort);
}
var authFile = getBeaconAuthFile();
if (!Files.exists(authFile)) {
effectiveBeaconPort = getDefaultBeaconPort();
return OptionalInt.of(effectiveBeaconPort);
}
if (effectiveBeaconPort != null) {
return OptionalInt.of(effectiveBeaconPort);
}
var hasEnv = System.getenv("BEACON_PORT") != null || System.getenv("XPIPE_BEACON_PORT") != null;
if (hasEnv) {
return OptionalInt.empty();
}
var start = 21723;
for (int i = 0; i < 20; i++) {
var p = start + i;
var occupied = BeaconServer.isReachable(p);
if (!occupied) {
effectiveBeaconPort = p;
return OptionalInt.of(p);
}
}
return OptionalInt.empty();
}
private void clearLeftoverAuthFile() {
var hasAuthFile = Files.exists(beaconAuthFile);
if (hasAuthFile) {
try (var channel = new RandomAccessFile(beaconLockFile.toFile(), "rw").getChannel()) {
var lock = channel.tryLock();
if (lock != null) {
lock.release();
Files.delete(beaconAuthFile);
}
} catch (Exception ignored) {
}
}
}
private static int getDefaultBeaconPort(boolean staging) {
var customPortVar = System.getenv("XPIPE_BEACON_PORT");
Integer customPort = null;
if (customPortVar != null) {
try {
customPort = Integer.parseInt(customPortVar);
} catch (NumberFormatException ignored) {
}
}
var rawPortProp = System.getProperty(AppNames.propertyName("beaconPort"));
if (customPort == null && rawPortProp != null) {
try {
var parsed = Integer.parseInt(rawPortProp);
customPort = parsed;
} catch (NumberFormatException ignored) {
}
}
var effectivePort = customPort != null ? customPort : 21721 + (staging ? 1 : 0);;
return effectivePort;
}
private static Path getLocalBeaconAuthFile(boolean staging) {
if (OsType.ofLocal() == OsType.LINUX) {
var name = AppSystemInfo.ofCurrent().getUser();
return AppSystemInfo.ofCurrent().getTemp().resolve(staging ? "xpipe-ptb" : "xpipe", name, "beacon-auth");
} else {
var path = AppSystemInfo.ofCurrent().getTemp().resolve(staging ? "xpipe-ptb" : "xpipe", "beacon-auth");
return path;
}
}
private static boolean isJUnitTest() {
@@ -4,6 +4,10 @@ import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.core.*;
import io.xpipe.app.core.check.AppDebugModeCheck;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.core.window.AppSideWindow;
import io.xpipe.app.core.window.AppWindowStyle;
import io.xpipe.app.ext.CliProvider;
import io.xpipe.app.ext.ExtensionException;
import io.xpipe.app.issue.*;
import io.xpipe.app.platform.PlatformInit;
import io.xpipe.app.platform.PlatformState;
@@ -109,10 +113,24 @@ public abstract class AppOperationMode {
ErrorEventFactory.fromThrowable(ex).unhandled(true).build().handle();
});
AppProperties.init(args);
if (AppProperties.get().isCli()) {
CliProvider.init(ModuleLayer.boot());
var cli = CliProvider.get();
if (cli == null) {
throw ExtensionException.corrupt("Missing cli module");
}
var r = cli.execute(args);
if (AppProperties.get().isAotTrainMode()) {
r = 0;
}
halt(r);
return;
}
TrackEvent.info("Initial setup");
AppMainWindow.loadingText("initializingApp");
GlobalTimer.init();
AppProperties.init(args);
PlatformThreadWatcher.init();
AppLogs.init();
AppDebugModeCheck.printIfNeeded();
@@ -273,7 +291,6 @@ public abstract class AppOperationMode {
public static void halt(int code) {
synchronized (HALT_LOCK) {
TrackEvent.info("Halting now!");
AppLogs.teardown();
Runtime.getRuntime().halt(code);
}
@@ -0,0 +1,22 @@
package io.xpipe.app.ext;
import java.util.OptionalInt;
import java.util.ServiceLoader;
public abstract class CliProvider {
private static CliProvider INSTANCE;
public static void init(ModuleLayer layer) {
INSTANCE = ServiceLoader.load(layer, CliProvider.class).stream()
.map(p -> p.get())
.findFirst()
.orElse(null);
}
public static CliProvider get() {
return INSTANCE;
}
public abstract int execute(String[] args);
}
+1
View File
@@ -128,6 +128,7 @@ open module io.xpipe.app {
uses ProcessControlProvider;
uses ShellDialect;
uses CloudSetupProvider;
uses CliProvider;
provides ActionProvider with
GradleRunMenuProvider,
@@ -12,10 +12,20 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
public class BeaconClient {
private static final String PRINT_MESSAGES_PROPERTY = "io.xpipe.beacon.printMessages";
private static boolean printMessages() {
if (System.getProperty(PRINT_MESSAGES_PROPERTY) != null) {
return Boolean.parseBoolean(System.getProperty(PRINT_MESSAGES_PROPERTY));
}
return false;
}
private final int port;
private String token;
@@ -23,9 +33,9 @@ public class BeaconClient {
this.port = port;
}
public static BeaconClient establishConnection(int port, BeaconClientInformation information) throws Exception {
public static BeaconClient establishConnection(int port, BeaconClientInformation information, Path authFile) throws Exception {
var client = new BeaconClient(port);
var auth = Files.readString(BeaconConfig.getLocalBeaconAuthFile());
var auth = Files.readString(authFile);
HandshakeExchange.Response response = client.performRequest(HandshakeExchange.Request.builder()
.client(information)
.auth(BeaconAuthMethod.Local.builder().authFileContent(auth).build())
@@ -34,19 +44,11 @@ public class BeaconClient {
return client;
}
public static Optional<BeaconClient> tryEstablishConnection(int port, BeaconClientInformation information) {
try {
return Optional.of(establishConnection(port, information));
} catch (Exception ex) {
return Optional.empty();
}
}
@SuppressWarnings("unchecked")
public <RES> RES performRequest(BeaconInterface<?> prov, String rawNode)
throws BeaconConnectorException, BeaconClientException, BeaconServerException {
var content = rawNode;
if (BeaconConfig.printMessages()) {
if (printMessages()) {
System.out.println("Sending raw request:");
System.out.println(content);
}
@@ -70,7 +72,7 @@ public class BeaconClient {
throw new BeaconConnectorException("Couldn't send request", ex);
}
if (BeaconConfig.printMessages()) {
if (printMessages()) {
System.out.println("Received raw response:");
System.out.println(response.body());
}
@@ -117,7 +119,7 @@ public class BeaconClient {
if (prov.isEmpty()) {
throw new IllegalArgumentException("Unknown request class " + req.getClass());
}
if (BeaconConfig.printMessages()) {
if (printMessages()) {
System.out.println(
"Sending request to server of type " + req.getClass().getName());
}
@@ -1,100 +0,0 @@
package io.xpipe.beacon;
import io.xpipe.core.OsType;
import lombok.experimental.UtilityClass;
import java.nio.file.Path;
import java.util.Optional;
import java.util.OptionalInt;
@UtilityClass
public class BeaconConfig {
public static final String BEACON_PORT_PROP = "io.xpipe.beacon.port";
private static final String PRINT_MESSAGES_PROPERTY = "io.xpipe.beacon.printMessages";
private static Integer portOverride;
public static boolean printMessages() {
if (System.getProperty(PRINT_MESSAGES_PROPERTY) != null) {
return Boolean.parseBoolean(System.getProperty(PRINT_MESSAGES_PROPERTY));
}
return false;
}
public static int getUsedPort() {
if (portOverride != null) {
return portOverride;
}
var beaconPort = System.getenv("BEACON_PORT");
if (beaconPort != null && !beaconPort.isBlank()) {
return Integer.parseInt(beaconPort);
}
if (System.getProperty(BEACON_PORT_PROP) != null) {
return Integer.parseInt(System.getProperty(BEACON_PORT_PROP));
}
return getDefaultBeaconPort();
}
public static OptionalInt fallBackToAnotherPort() {
var hasEnv = System.getenv("BEACON_PORT") != null || System.getenv("XPIPE_BEACON_PORT") != null;
if (hasEnv) {
return OptionalInt.empty();
}
var start = 21723;
for (int i = 0; i < 20; i++) {
var p = start + i;
var occupied = BeaconServer.isReachable(p);
if (!occupied) {
portOverride = p;
return OptionalInt.of(p);
}
}
return OptionalInt.empty();
}
public static int getDefaultBeaconPort() {
var customPortVar = System.getenv("XPIPE_BEACON_PORT");
Integer customPort = null;
if (customPortVar != null) {
try {
customPort = Integer.parseInt(customPortVar);
} catch (NumberFormatException ignored) {
}
}
var effectivePortBase = customPort != null ? customPort : 21721;
var staging = Optional.ofNullable(System.getProperty("io.xpipe.app.staging"))
.map(Boolean::parseBoolean)
.orElse(false);
var offset = staging ? 1 : 0;
return effectivePortBase + offset;
}
public static Path getLocalBeaconAuthFile() {
var staging = Optional.ofNullable(System.getProperty("io.xpipe.app.staging"))
.map(Boolean::parseBoolean)
.orElse(false);
if (OsType.ofLocal() == OsType.LINUX) {
var name = System.getenv("USER") != null ? System.getenv("USER") : System.getProperty("user.name");
return Path.of(System.getProperty("java.io.tmpdir"), staging ? "xpipe-ptb" : "xpipe", name, "beacon-auth");
} else {
var path = Path.of(System.getProperty("java.io.tmpdir"), staging ? "xpipe-ptb" : "xpipe", "beacon-auth");
if (path.startsWith(Path.of("C:\\Windows"))) {
path = Path.of(System.getenv("LOCALAPPDATA"))
.resolve("Temp", staging ? "xpipe-ptb" : "xpipe", "beacon-auth");
}
return path;
}
}
public static Path getLocalBeaconLockFile() {
return getLocalBeaconAuthFile().getParent().resolve("lock");
}
}
+1 -1
View File
@@ -224,7 +224,7 @@ project.ext {
ci = System.getenv('CI') != null
obfuscate = true
fullVersion = file("$rootDir/private_files.txt").exists()
bundleCds = ci && fullVersion
bundleCds = true
// Names
productName = isStage ? 'XPipe PTB' : 'XPipe'
-1
View File
@@ -96,7 +96,6 @@ apply from: 'jpackage.gradle'
if (fullVersion) {
apply from: 'train.gradle'
apply from: 'base_full.gradle'
apply from: 'cli.gradle'
apply from: 'portable.gradle'
apply from: 'proguard.gradle'
apply from: 'github.gradle'
+15 -1
View File
@@ -14,6 +14,9 @@ def extLibrariesTasks = extModules.stream().map { it.getTasksByName('createExtOu
dependencies {
implementation project(':app')
if (fullVersion) {
implementation project(':cli')
}
if (!useBundledJavaFx) {
configurations.javafx.getAsFileTree().getFiles().forEach {
implementation files(it)
@@ -67,11 +70,22 @@ jlink {
addOptions("--add-modules", "jdk.jdwp.agent")
}
if (fullVersion) {
addOptions("--add-modules", "io.xpipe.cli")
}
launcher {
moduleName = packageName(null)
mainClass = packageName('Main')
name = jpackageExecutableName
jvmArgs = jpackageReleaseArguments
jvmArgs = jpackageReleaseArguments + ["-Dio.xpipe.app.isDaemon=true"]
}
secondaryLauncher {
moduleName = packageName(null)
mainClass = packageName('Main')
name = "xpipe"
jvmArgs = jpackageReleaseArguments + ["-Dio.xpipe.app.isCli=true"]
}
jpackage {
+1 -1
View File
@@ -187,7 +187,7 @@ Write-Host "$ProductName has been successfully installed. You should be able to
Write-Host
# Use absolute path as we can't assume that the user has selected to put XPipe into the Path
& "$env:LOCALAPPDATA\$ProductName\bin\xpipe.exe" open
& "$env:LOCALAPPDATA\$ProductName\xpiped.exe" open
#endregion Install XPipe
@@ -18,7 +18,6 @@ testing {
testTask.configure {
workingDir = rootDir
jvmArgs += ["-Xmx2g"]
jvmArgs += jvmRunArgs
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList())
-20
View File
@@ -1,23 +1,3 @@
extraJavaModuleInfo {
module("de.vandermeer:asciitable", "de.vandermeer.asciitable") {
exportAllPackages()
requires('de.vandermeer.skb_interfaces')
requires('de.vandermeer.ascii_utf_themes')
requires('org.apache.commons.lang3')
}
module("de.vandermeer:skb-interfaces", "de.vandermeer.skb_interfaces") {
exportAllPackages()
requires('org.apache.commons.lang3')
}
module("de.vandermeer:ascii-utf-themes", "de.vandermeer.ascii_utf_themes") {
exportAllPackages()
requires('org.apache.commons.lang3')
requires('de.vandermeer.skb_interfaces')
}
}
extraJavaModuleInfo {
module("com.vladsch.flexmark:flexmark", "com.vladsch.flexmark") {
mergeJar('com.vladsch.flexmark:flexmark-util')
@@ -18,7 +18,6 @@ testing {
testTask.configure {
workingDir = projectDir
jvmArgs += ["-Xmx2g"]
jvmArgs += jvmRunArgs
def daemonArgs = Map.of(