This commit is contained in:
crschnick
2026-05-25 15:09:43 +00:00
parent efc226667f
commit e7724340e5
28 changed files with 231 additions and 92 deletions
+2 -2
View File
@@ -97,7 +97,7 @@ jar {
application {
mainModule = groupName + '.app'
mainClass = groupName + '.app.Main'
applicationDefaultJvmArgs = jvmRunArgs
applicationDefaultJvmArgs = daemonJvmRunArgs
}
run {
@@ -136,7 +136,7 @@ tasks.register('runAttachedDebugger', JavaExec) {
mainModule = groupName + '.app'
mainClass = groupName + '.app.Main'
modularity.inferModulePath = true
jvmArgs += jvmRunArgs
jvmArgs += daemonJvmRunArgs
jvmArgs += List.of(
"-javaagent:${System.getProperty("user.home")}/.attachme/attachme-agent-1.2.9.jar=port:7857,host:localhost".toString(),
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0"
@@ -59,5 +59,10 @@ public interface ActionProvider {
.map(p -> p.get())
.toList());
}
@Override
public boolean initForCli() {
return false;
}
}
}
@@ -57,7 +57,7 @@ public class AppBeaconServer {
public static void init() {
try {
// 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 = new AppBeaconServer(AppProperties.get().queryEffectiveBeaconPort(false).orElseThrow());
INSTANCE.initAuthSecret();
INSTANCE.start();
TrackEvent.withInfo("Started http server")
@@ -20,13 +20,15 @@ public class AppArguments {
private static final Pattern PROPERTY_PATTERN = Pattern.compile("^-[DP](.+)=(.+)$");
List<String> rawArgs;
List<String> resolvedArgs;
List<String> openArgs;
List<String> daemonOpenArgs;
public static AppArguments init(String[] args) {
var rawArgs = Arrays.asList(args);
var resolvedArgs = Arrays.asList(parseProperties(args));
var command = LauncherCommand.resolveLauncher(resolvedArgs.toArray(String[]::new));
return new AppArguments(rawArgs, resolvedArgs, command.inputs);
var isDaemon = Boolean.getBoolean("io.xpipe.app.isDaemon");
var openArgs = !isDaemon ? List.<String>of() :
LauncherCommand.resolveLauncher(resolvedArgs.toArray(String[]::new)).inputs;
return new AppArguments(rawArgs, resolvedArgs, openArgs);
}
private static String[] parseProperties(String[] args) {
@@ -71,7 +71,7 @@ public class AppInstance {
}
try {
var inputs = AppProperties.get().getArguments().getOpenArgs();
var inputs = AppProperties.get().getArguments().getDaemonOpenArgs();
// Assume that we want to open the GUI if we launched again
client.get().performRequest(DaemonFocusExchange.Request.builder().build());
if (!inputs.isEmpty()) {
@@ -21,7 +21,7 @@ public class AppOpenArguments {
private static final List<String> bufferedArguments = new ArrayList<>();
public static synchronized void init() {
handleImpl(AppProperties.get().getArguments().getOpenArgs());
handleImpl(AppProperties.get().getArguments().getDaemonOpenArgs());
handleImpl(bufferedArguments);
bufferedArguments.clear();
}
@@ -97,6 +97,7 @@ public class AppProperties {
.getLocation()
.getProtocol()
.equals("jrt");
arguments = AppArguments.init(args);
fullVersion = Optional.ofNullable(System.getProperty(AppNames.propertyName("fullVersion")))
.map(Boolean::parseBoolean)
.orElse(false);
@@ -178,7 +179,6 @@ public class AppProperties {
isDaemon = Optional.ofNullable(System.getProperty(AppNames.propertyName("isDaemon")))
.map(Boolean::parseBoolean)
.orElse(!isCli);
arguments = AppArguments.init(isDaemon ? args : new String[0]);
// We require the user dir from here
AppDirectoryPermissionsCheck.checkDirectory(dataDir);
@@ -211,24 +211,25 @@ public class AppProperties {
}
public OptionalInt queryEffectiveBeaconPort(boolean reachable) {
if (effectiveBeaconPort != null) {
return OptionalInt.of(effectiveBeaconPort);
}
if (!reachable) {
effectiveBeaconPort = getDefaultBeaconPort();
return OptionalInt.of(effectiveBeaconPort);
}
var authFile = getBeaconAuthFile();
if (!Files.exists(authFile)) {
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();
effectiveBeaconPort = getDefaultBeaconPort();
return OptionalInt.of(effectiveBeaconPort);
}
var start = 21723;
@@ -330,7 +331,7 @@ public class AppProperties {
TrackEvent.withInfo("Received arguments")
.tag("raw", arguments.getRawArgs())
.tag("resolved", arguments.getResolvedArgs())
.tag("resolvedCommand", arguments.getOpenArgs())
.tag("resolvedCommand", arguments.getDaemonOpenArgs())
.handle();
for (var e : System.getProperties().entrySet()) {
@@ -20,6 +20,7 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.update.AppDistributionType;
import io.xpipe.app.util.*;
import io.xpipe.core.FailableRunnable;
import io.xpipe.core.ModuleLayerLoader;
import io.xpipe.core.XPipeDaemonMode;
import javafx.application.Platform;
@@ -115,12 +116,12 @@ public abstract class AppOperationMode {
AppProperties.init(args);
if (AppProperties.get().isCli()) {
CliProvider.init(ModuleLayer.boot());
ModuleLayerLoader.loadAll(ModuleLayer.boot(), throwable -> throwable.printStackTrace());
var cli = CliProvider.get();
if (cli == null) {
throw ExtensionException.corrupt("Missing cli module");
}
var r = cli.execute(args);
var r = cli.execute(AppProperties.get().getArguments().getResolvedArgs().toArray(String[]::new));
if (AppProperties.get().isAotTrainMode()) {
r = 0;
}
@@ -1,22 +1,35 @@
package io.xpipe.app.ext;
import io.xpipe.beacon.BeaconInterface;
import io.xpipe.core.ModuleLayerLoader;
import java.util.OptionalInt;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
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);
public static class Loader implements ModuleLayerLoader {
@Override
public void init(ModuleLayer layer) {
INSTANCE = ServiceLoader.load(layer, CliProvider.class).stream()
.map(p -> p.get())
.findFirst()
.orElse(null);
}
@Override
public boolean initForCli() {
return true;
}
}
}
@@ -32,5 +32,10 @@ public interface CloudSetupProvider {
.map(p -> p.get())
.toList());
}
@Override
public boolean initForCli() {
return false;
}
}
}
@@ -27,5 +27,10 @@ public abstract class DataStorageExtensionProvider {
scanProvider -> scanProvider.getClass().getName()))
.collect(Collectors.toList());
}
@Override
public boolean initForCli() {
return false;
}
}
}
@@ -90,5 +90,10 @@ public class DataStoreProviders {
});
}
}
@Override
public boolean initForCli() {
return false;
}
}
}
@@ -36,5 +36,10 @@ public abstract class PrefsProvider {
.map(ServiceLoader.Provider::get)
.collect(Collectors.toList());
}
@Override
public boolean initForCli() {
return false;
}
}
}
@@ -43,6 +43,11 @@ public abstract class ScanProvider {
scanProvider -> scanProvider.getClass().getName()))
.collect(Collectors.toList());
}
@Override
public boolean initForCli() {
return false;
}
}
@Value
@@ -71,9 +71,7 @@ public class ShellDialects {
@Override
public void init(ModuleLayer layer) {
var services = layer != null
? ServiceLoader.load(layer, ShellDialect.class)
: ServiceLoader.load(ShellDialect.class);
var services = ServiceLoader.load(layer, ShellDialect.class);
services.stream().forEach(moduleLayerLoaderProvider -> {
ALL.add(moduleLayerLoaderProvider.get());
});
@@ -108,5 +106,10 @@ public class ShellDialects {
HETZNER_BOX = byId("hetznerBox");
SFTP = byId("sftp");
}
@Override
public boolean initForCli() {
return false;
}
}
}
@@ -3,11 +3,16 @@ package io.xpipe.app.update;
import io.xpipe.app.comp.RegionBuilder;
import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.TrackEvent;
public class UpdateAvailableDialog {
public static boolean showIfNeeded(boolean wait) {
if (AppProperties.get().isAotTrainMode()) {
return false;
}
UpdateHandler uh = AppDistributionType.get().getUpdateHandler();
if (uh.getPreparedUpdate().getValue() == null) {
return false;
@@ -1,6 +1,7 @@
package io.xpipe.app.util;
import io.xpipe.app.comp.BaseRegionBuilder;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.ext.ExtensionException;
import io.xpipe.core.ModuleLayerLoader;
@@ -49,5 +50,10 @@ public abstract class LicenseProvider {
.findFirst()
.orElseThrow(() -> ExtensionException.corrupt("Missing license provider"));
}
@Override
public boolean initForCli() {
return false;
}
}
}
+2 -1
View File
@@ -217,7 +217,8 @@ open module io.xpipe.app {
LicenseProvider.Loader,
ScanProvider.Loader,
ShellDialects.Loader,
CloudSetupProvider.Loader;
CloudSetupProvider.Loader,
CliProvider.Loader;
provides SLF4JServiceProvider with
AppLogs.Slf4jProvider;
provides EventHandler with
@@ -78,9 +78,7 @@ public abstract class BeaconInterface<T> {
@Override
public void init(ModuleLayer layer) {
var services = layer != null
? ServiceLoader.load(layer, BeaconInterface.class)
: ServiceLoader.load(BeaconInterface.class);
var services = ServiceLoader.load(layer, BeaconInterface.class);
ALL = services.stream()
.map(ServiceLoader.Provider::get)
.map(beaconInterface -> (BeaconInterface<?>) beaconInterface)
@@ -90,5 +88,10 @@ public abstract class BeaconInterface<T> {
.anyMatch(other -> !other.equals(beaconInterface)
&& beaconInterface.getClass().isAssignableFrom(other.getClass())));
}
@Override
public boolean initForCli() {
return true;
}
}
}
@@ -1 +0,0 @@
io.xpipe.beacon.BeaconInterface$Loader
+120 -47
View File
@@ -101,14 +101,18 @@ static def getPlatformName() {
return platform
}
def getJvmArgs() {
def getBaseJvmArgs() {
def os = DefaultNativePlatform.currentOperatingSystem
def jvmRunArgs = [
"-Dfile.encoding=UTF-8",
"-Dvisualvm.display.name=$productName",
"-Djavafx.preloader=" + packageName("core.AppPreloader"),
"-Djdk.virtualThreadScheduler.parallelism=8"
]
def jvmRunArgs = []
// Force UTF8 encoding. This isn't really necessary anymore in JDK25+
jvmRunArgs += ["-Dfile.encoding=UTF-8"]
// Make the app show up nicely in VisualVM
jvmRunArgs += ["-Dvisualvm.display.name=$productName"]
// Make virtual threads behave the same, independently of CPU core count
jvmRunArgs += ["-Djdk.virtualThreadScheduler.parallelism=8"]
// Virtual threads cause crashes on Windows ARM
if (os.isWindows() && arch == "arm64") {
@@ -119,9 +123,6 @@ def getJvmArgs() {
// Disable JDK24 warnings
jvmRunArgs += [
"--enable-native-access=com.sun.jna",
"--enable-native-access=javafx.graphics",
"--enable-native-access=javafx.web",
"--sun-misc-unsafe-memory-access=allow",
]
@@ -130,10 +131,50 @@ def getJvmArgs() {
jvmRunArgs += [
"--add-opens", "java.base/java.lang=$appPackage",
"--add-opens", "java.base/java.net=$appPackage",
"--add-opens", "net.synedra.validatorfx/net.synedra.validatorfx=$appPackage",
"--add-opens", "java.base/java.nio.file=$appPackage",
"--add-exports", "javafx.graphics/com.sun.javafx.tk=$appPackage",
"--add-exports", "jdk.zipfs/jdk.nio.zipfs=io.xpipe.modulefs",
]
// Use project liliput
jvmRunArgs += ['-XX:+UseCompactObjectHeaders']
// Reduce heap usage with deduplication
jvmRunArgs += ['-XX:+UseStringDeduplication']
// Why is this not on by default? ...
// https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/net/doc-files/net-properties.html
// https://stackoverflow.com/questions/53333556/proxy-authentication-with-jdk-11-httpclient
// https://stackoverflow.com/questions/75150081/ioexception-too-many-authentication-attempts-limit-3-when-using-jdk-httpcli
jvmRunArgs += [
'-Djava.net.useSystemProxies=true',
'-Djdk.http.auth.proxying.disabledSchemes=""',
'-Djdk.http.auth.tunneling.disabledSchemes=""',
'-Djdk.httpclient.auth.retrylimit=1'
]
return jvmRunArgs
}
def getDaemonJvmArgs() {
def os = DefaultNativePlatform.currentOperatingSystem
def jvmRunArgs = ["-Dio.xpipe.app.isDaemon=true"]
// Use custom JavaFX preloader
jvmRunArgs += ["-Djavafx.preloader=" + packageName("core.AppPreloader")]
// Disable JDK24 warnings
jvmRunArgs += [
"--enable-native-access=com.sun.jna",
"--enable-native-access=javafx.graphics",
"--enable-native-access=javafx.web"
]
// Module access fixes
def appPackage = packageName(null)
jvmRunArgs += [
"--add-opens", "net.synedra.validatorfx/net.synedra.validatorfx=$appPackage",
"--add-exports", "javafx.graphics/com.sun.javafx.tk=$appPackage",
"--add-opens", "javafx.graphics/com.sun.glass.ui=$appPackage",
"--add-opens", "javafx.graphics/javafx.stage=$appPackage",
"--add-opens", "javafx.controls/javafx.scene.control.skin=$appPackage",
@@ -148,12 +189,6 @@ def getJvmArgs() {
]
}
// Use project liliput
jvmRunArgs += ['-XX:+UseCompactObjectHeaders']
// Reduce heap usage with deduplication
jvmRunArgs += ['-XX:+UseStringDeduplication']
// GC config
jvmRunArgs += [
'-XX:+UseG1GC',
@@ -166,17 +201,6 @@ def getJvmArgs() {
'-XX:G1HeapRegionSize=4m'
]
// Why is this not on by default? ...
// https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/net/doc-files/net-properties.html
// https://stackoverflow.com/questions/53333556/proxy-authentication-with-jdk-11-httpclient
// https://stackoverflow.com/questions/75150081/ioexception-too-many-authentication-attempts-limit-3-when-using-jdk-httpcli
jvmRunArgs += [
'-Djava.net.useSystemProxies=true',
'-Djdk.http.auth.proxying.disabledSchemes=""',
'-Djdk.http.auth.tunneling.disabledSchemes=""',
'-Djdk.httpclient.auth.retrylimit=1'
]
// Fix platform theme detection on macOS
if (os.isMacOsX()) {
jvmRunArgs += ["-Dapple.awt.application.appearance=system"]
@@ -196,6 +220,69 @@ def getJvmArgs() {
jvmRunArgs.addAll("--add-opens", "java.desktop/sun.awt.X11=" + packageName(null))
}
return getBaseJvmArgs() + jvmRunArgs
}
def getCliJvmArgs() {
def jvmRunArgs = ["-Dio.xpipe.app.isCli=true"]
// GC config
jvmRunArgs += [
'-XX:+UseG1GC',
'-Xms50m',
'-Xmx250m',
// The default makes GC pauses longer for some reason
'-XX:G1HeapRegionSize=4m'
]
return getBaseJvmArgs() + jvmRunArgs
}
def getDaemonJPackageReleaseJvmArgs() {
def jvmRunArgs = getDaemonJvmArgs()
jvmRunArgs += [
"-D" + propertyName("version") + "=" + versionString,
"-D" + propertyName("build") + "=$versionString/${new Date().format('yyyy-MM-dd-HH-mm')}",
"-D" + propertyName("buildId") + "=" + buildId,
"-D" + propertyName("fullVersion") + "=" + fullVersion,
"-D" + propertyName("staging") + "=" + isStage,
"-D" + propertyName("sentryUrl") + "=" + sentryUrl
]
jvmRunArgs += [
'-Djna.nosys=false',
'-Djna.nounpack=true',
'-Djna.noclasspath=true'
]
if (os.isMacOsX()) {
jvmRunArgs += "-Xdock:name=$productName"
}
if (isFullRelease || isStage) {
jvmRunArgs += "-XX:+DisableAttachMechanism"
}
return jvmRunArgs
}
def getCliJPackageReleaseJvmArgs() {
def jvmRunArgs = getCliJvmArgs()
jvmRunArgs += [
"-D" + propertyName("version") + "=" + versionString,
"-D" + propertyName("build") + "=$versionString/${new Date().format('yyyy-MM-dd-HH-mm')}",
"-D" + propertyName("buildId") + "=" + buildId,
"-D" + propertyName("fullVersion") + "=" + fullVersion,
"-D" + propertyName("staging") + "=" + isStage,
"-D" + propertyName("sentryUrl") + "=" + sentryUrl
]
if (isFullRelease || isStage) {
jvmRunArgs += "-XX:+DisableAttachMechanism"
}
return jvmRunArgs
}
@@ -274,30 +361,16 @@ project.ext {
groupName = 'io.xpipe'
artifactName = 'app'
arch = getArchName()
jvmRunArgs = getJvmArgs()
daemonJvmRunArgs = getDaemonJvmArgs()
cliJvmRunArgs = getCliJvmArgs()
useBundledJna = fullVersion
sentryUrl = "https://fd5f67ff10764b7e8a704bec9558c8fe@o1084459.ingest.sentry.io/6094279"
// JPackage config
jpackageExecutableName = "xpiped"
jpackageMacOsBundleName = isStage ? groupName + '.ptb-app' : groupName + '.app'
jpackageReleaseArguments = jvmRunArgs + [
"-D" + propertyName("version") + "=" + versionString,
"-D" + propertyName("build") + "=$versionString/${new Date().format('yyyy-MM-dd-HH-mm')}",
"-D" + propertyName("buildId") + "=" + buildId,
"-D" + propertyName("fullVersion") + "=" + fullVersion,
"-D" + propertyName("staging") + "=" + isStage,
"-D" + propertyName("sentryUrl") + "=" + sentryUrl,
'-Djna.nosys=false',
'-Djna.nounpack=true',
'-Djna.noclasspath=true'
]
if (os.isMacOsX()) {
jpackageReleaseArguments += "-Xdock:name=$productName"
}
if (isFullRelease || isStage) {
jpackageReleaseArguments += "-XX:+DisableAttachMechanism"
}
jpackageReleaseDaemonArguments = getDaemonJPackageReleaseJvmArgs()
jpackageReleaseCliArguments = getCliJPackageReleaseJvmArgs()
// JavaFX config
devJavafxVersion = '27-ea+4'
@@ -54,8 +54,7 @@ public class JacksonMapper {
private static List<Module> findModules(ModuleLayer layer) {
ArrayList<Module> modules = new ArrayList<>();
ServiceLoader<Module> loader =
layer != null ? ServiceLoader.load(layer, Module.class) : ServiceLoader.load(Module.class);
ServiceLoader<Module> loader = ServiceLoader.load(layer, Module.class);
for (Module module : loader) {
modules.add(module);
}
@@ -150,5 +149,10 @@ public class JacksonMapper {
INSTANCE.registerModules(modules);
init = true;
}
@Override
public boolean initForCli() {
return true;
}
}
}
@@ -6,10 +6,8 @@ import java.util.function.Consumer;
public interface ModuleLayerLoader {
static void loadAll(ModuleLayer layer, Consumer<Throwable> errorHandler) {
var loaded = layer != null
? ServiceLoader.load(layer, ModuleLayerLoader.class)
: ServiceLoader.load(ModuleLayerLoader.class);
loaded.stream().forEach(moduleLayerLoaderProvider -> {
var loaded = ServiceLoader.load(layer, ModuleLayerLoader.class);
loaded.stream().map(ServiceLoader.Provider::get).filter(p -> p.initForCli() || !AppPro).forEach(moduleLayerLoaderProvider -> {
var instance = moduleLayerLoaderProvider.get();
try {
instance.init(layer);
@@ -20,4 +18,6 @@ public interface ModuleLayerLoader {
}
default void init(ModuleLayer layer) {}
boolean initForCli();
}
@@ -1 +0,0 @@
io.xpipe.core.CoreJacksonModule
@@ -1 +0,0 @@
io.xpipe.core.JacksonMapper$Loader
+2 -2
View File
@@ -78,14 +78,14 @@ jlink {
moduleName = packageName(null)
mainClass = packageName('Main')
name = jpackageExecutableName
jvmArgs = jpackageReleaseArguments + ["-Dio.xpipe.app.isDaemon=true"]
jvmArgs = jpackageReleaseDaemonArguments
}
secondaryLauncher {
moduleName = packageName(null)
mainClass = packageName('Main')
name = "xpipe"
jvmArgs = jpackageReleaseArguments + ["-Dio.xpipe.app.isCli=true"]
jvmArgs = jpackageReleaseCliArguments
}
jpackage {
@@ -18,7 +18,7 @@ testing {
testTask.configure {
workingDir = rootDir
jvmArgs += jvmRunArgs
jvmArgs += daemonJvmRunArgs
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList())
classpath += exts
@@ -18,7 +18,7 @@ testing {
testTask.configure {
workingDir = projectDir
jvmArgs += jvmRunArgs
jvmArgs += daemonJvmRunArgs
def daemonArgs = Map.of(
propertyName("dataDir"), "$projectDir/local/",