Can command-line set Java system properties be distinguished from the defaults? - java

Is there a way to distinguish a Java system property which has been set from the command line using a -D option from a system property which got the same value by default?
E.g., this program
class Test {
public static void main(String[] args) {
System.out.println(System.getProperty("user.home"));
}
}
prints
/home/uckelman
for me regardless of whether I run it as
java Test
or
java -Duser.home=/home/uckelman Test
Is there anything which the JDK provides which I could test to distinguish these two situations?

One way is to get the command line arguments used to start the JVM and check the returned List if a given system property is set or not.
RuntimeMXBean mx = ManagementFactory.getRuntimeMXBean();
System.out.println("COMMAND LINE ARGS:\n" + mx.getInputArguments());

Related

Get full command line from Java [duplicate]

This question already has answers here:
Determining location of JVM executable during runtime
(5 answers)
Closed 10 months ago.
The community reviewed whether to reopen this question 27 days ago and left it closed:
Original close reason(s) were not resolved
How can I from within Java code find out which and how the running JVM was launched?
My code shall spawn another Java process hence I'd be especially interested in the first parameter - which usually (in C, Pascal, Bash, Python, ...) points to the currently running executable.
So when a Java application is run like
d:\openjdk\bin\java -Xmx500g -Dprop=name -jar my.jar param1 param2
I can access command line parameters in my main method like
public class Main {
public static void main(String[] args) {
System.out.println("main called with " + args);
}
}
but that will deliver access to param1 and param2 only. How would I get the full command line?
Following Determining location of JVM executable during runtime I found the real answer to be:
ProcessHandle.current().info().commandLine().orElseThrow();
It returns the full command line as perceived by the operating system and thus contains all information: executable, options, main class, arguments, ...
Thank you, #XtremeBaumer
To get the command, use ProcessHandle adapted from your answer:
String command = ProcessHandle
.current()
.info()
.command()
.orElse("<unable to determine command>"); // .orElseThrow() to get Exception
Use the Runtime Management Bean RuntimeMXBean to obtain the VM arguments and the class path, like in:
RuntimeMXBean mxBean = ManagementFactory.getRuntimeMXBean();
String classPath = mxBean.getClassPath();
List<String> inputArgs = mxBean.getInputArguments();
This will return all arguments passed to the virtual machine, not the parameters passed to the main method.
The code source can be get from the ProtectionDomain of the class.

In picocli how do you make options on the command line to override the same option in an #-file

I'm currently using picocli 4.7.0-SNAPSHOT to great effect in a Java 11 application that has a complex set of options, so I am making use of the #-file functionality.
What I am trying to get to work is an option specified directly on the command line to override the same option if it exists in the #-file. So options specified on the command line take precedence over the #-file. Is that possible.
When I try to run my test application, heavily based on the picocli example, with both a command line option and an #-file, I get the following error from picocli along with the expected usage:
myapp --sourceDatabaseType=MySQL #.\myapp.options
option '--sourceDatabaseType' (<sourceDatabaseType>) should be specified only once
and then the expected usage information.
Let me paraphrase the question to see if I understand it correctly:
If the end user specifies an option directly on the command line, the command line value should be used, while if that option is not specified on the command line, the value in a file should be used.
Essentially, you are using an #-file with the intention to define default values for one or more options.
However, that is not what #-files were designed for: picocli cannot distinguish between arguments that came from the command line and arguments that came from the #-file.
I would suggest using picocli's default provider mechanism instead.
One idea is to use the built-in PropertiesDefaultProvider:
import picocli.CommandLine.PropertiesDefaultProvider;
#Command(name = "myapp", defaultValueProvider = PropertiesDefaultProvider.class)
class MyApp { }
PropertiesDefaultProvider also uses a file, and the values in that file are only used for options that were not specified on the command line.
The tricky bit is the location of this file. PropertiesDefaultProvider looks for the file in the following locations:
the path specified by system property picocli.defaults.${COMMAND-NAME}.path
a file named .${COMMAND-NAME}.properties in the end user's user home directory
(Replace ${COMMAND-NAME} by the name of the command, so for a command named myapp, the system property is picocli.defaults.myapp.path)
To give end users the ability to specify the location of the file, we need to set the system property before picocli completes parsing the command line arguments.
We can do that with an #Option-annotated setter method. For example:
class MyApp {
#Option(names = "-D")
void setSystemProperty(Map<String, String> properties) {
System.getProperties().putAll(properties);
}
}
This would allow end users to invoke the command with something like this:
myapp --sourceDatabaseType=MySQL -Dpicocli.defaults.myapp.path=.\myapp.options
If this is too verbose, you could go one step further and create a special -# option, to allow users to invoke the command with something like this:
myapp --sourceDatabaseType=MySQL -#.\myapp.options
The implementation for this would be an annotated setter method, similar to the above:
class MyApp {
#Spec CommandSpec spec; // injected by picocli
#Option(names = "-#")
void setDefaultProviderPath(File path) {
// you could do some validation here:
if (!path.canRead()) {
String msg = String.format("ERROR: file not found: %s", path);
throw new ParameterException(spec.commandLine(), msg);
}
// only set the system property if the file exists
System.setProperty("picocli.defaults.myapp.path", path.toString());
}
}

How to configure lightbend/typesafeConfig via command line parameter when using picocli

In our project we are using Lightbend Config / TypesafeConfig.
I can run my program with java -jar. The configuration of my program can also be done by using command line parameters.
Example:
java -jar simpleclient.jar -Dservice.url="http://localhost:8123"
Now I introduced https://picocli.info/ to have a better command line handling for my application.
The problem I'm facing now ist, that picocli doesn't allow the usage of -D... parameters in the standard configuration.
How can this be changed?
When you say “picocli doesn’t allow the use of -D... options”, I assume you mean you want to allow end users to set system properties with the -Dkey=value syntax. When such parameters are passed to the application, the application needs to use these values to set system properties, as shown below.
First, users can set system properties by passing -Dkey=value parameters to the java process instead of to the main class in the jar. In the below invocation, the system properties are set directly and are not passed as parameters to the application:
java -Dservice.url="http://localhost:8123" -jar simpleclient.jar
Secondly, you can define a -D option in your application that sets system properties:
#Command
class SimpleClient {
#Option(names = "-D")
void setProperty(Map<String, String> props) {
props.forEach((k, v) -> System.setProperty(k, v == null ? "" : v));
}
}

System.getenv() - Behavior depends on whether debug mode is enabled

I try to retrieve the windows directory of my OS.
To get the correct path I tried the following 2 commands:
System.getenv().get("WINDIR")
System.getenv().get("SystemRoot")
Both commands work, but the strange thing is, that the first command (WINDIR) returns the path only, if I
run the program in debug mode. The latter command (SystemRoot) returns the path only if I run the program not in debug mode.
So this program
public static void main(String[] args) {
System.out.println(System.getenv().get("WINDIR"));
System.out.println(System.getenv().get("SystemRoot"));
}
evaluates to
// Debug mode
C:\Windows
null
// No Debug mode
null
C:\Windows
Is this a defined behavior?
(My application is Windows specific, and if i speak of debug mode I mean the default Eclipse "Debug as Java Applicaton" run configiguration)
System.getEnv() is an overloaded method, one implementation having no parameters and one having a String parameter.
static Map getenv​() Returns an unmodifiable string map view of the current system environment.
static String getenv​(String name) Gets the value of the specified environment variable.
You are calling the implementatation with no parameters, and then calling get() on the returned Map. From the Javadoc for System.getEnv():
For getEnv(): The returned map is typically case-sensitive on all platforms.
For getEnv(String): On UNIX systems the alphabetic case of name is typically significant, while on Microsoft Windows systems it is typically not.
So it is essential for your code to be providing the name of the environment variable in the correct case, specifying windir in all lower case, not upper case.
That said, I can't explain the differences you see when running in debug mode at all. If I run the program below - which is just an enhanced version of yours - I get identical results (as expected) regardless of whether it is run in debug mode or not:
System.getenv().get() windir=C:\WINDOWS
System.getenv().get() WINDIR=null
System.getenv().get() systemroot=null
System.getenv().get() SystemRoot=C:\WINDOWS
System.getenv() windir=C:\WINDOWS
System.getenv() WINDIR=C:\WINDOWS
System.getenv() systemroot=C:\WINDOWS
System.getenv() SystemRoot=C:\WINDOWS
Could you run the code below twice, once in debug mode and once in normal mode, and advise of the results? Also, advise of your environment: Windows version, Eclipse version and Java version.
[This is more of a request for further information than a final answer, but I couldn't fit it all into a comment.]
import java.lang.management.ManagementFactory;
import java.util.regex.Pattern;
public class App {
private final static Pattern debugPattern = Pattern.compile("-Xdebug|jdwp");
public static boolean isDebugging() {
// https://stackoverflow.com/questions/7397584/how-to-know-my-code-is-running-in-debug-mode-in-ide
// Taken from the code provided by SO user AlexR
for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
if (debugPattern.matcher(arg).find()) {
return true;
}
}
return false;
}
public static void main(String[] args) {
System.out.println("Running in debug mode? " + App.isDebugging());
System.out.println("System.getenv().get() windir=" + System.getenv().get("windir"));
System.out.println("System.getenv().get() WINDIR=" + System.getenv().get("WINDIR"));
System.out.println("System.getenv().get() systemroot=" + System.getenv().get("systemroot"));
System.out.println("System.getenv().get() SystemRoot=" + System.getenv().get("SystemRoot"));
System.out.println("System.getenv() windir=" + System.getenv("windir"));
System.out.println("System.getenv() WINDIR=" + System.getenv("WINDIR"));
System.out.println("System.getenv() systemroot=" + System.getenv("systemroot"));
System.out.println("System.getenv() SystemRoot=" + System.getenv("SystemRoot"));
}
}

Is *this* really the best way to start a second JVM from Java code?

This is a followup to my own previous question and I'm kind of embarassed to ask this... But anyway: how would you start a second JVM from a standalone Java program in a system-independent way? And without relying on for instance an env variable like JAVA_HOME as that might point to a different JRE than the one that is currently running. I came up with the following code which actually works but feels just a little awkward:
public static void startSecondJVM() throws Exception {
String separator = System.getProperty("file.separator");
String classpath = System.getProperty("java.class.path");
String path = System.getProperty("java.home")
+ separator + "bin" + separator + "java";
ProcessBuilder processBuilder =
new ProcessBuilder(path, "-cp",
classpath,
AnotherClassWithMainMethod.class.getName());
Process process = processBuilder.start();
process.waitFor();
}
Also, the currently running JVM might have been started with some other parameters (-D, -X..., ...) that the second JVM would not know about.
I think that the answer is "Yes". This probably as good as you can do in Java using system independent code. But be aware that even this is only relatively system independent. For example, in some systems:
the JAVA_HOME variable may not have been set,
the command name used to launch a JVM might be different (e.g. if it is not a Sun JVM), or
the command line options might be different (e.g. if it is not a Sun JVM).
If I was aiming for maximum portability in launching a (second) JVM, I think I would do it using wrapper scripts.
It's not clear to me that you would always want to use exactly the same parameters, classpath or whatever (especially -X kind of stuff - for example, why would the child need the same heap settings as its parents) when starting a secondary process.
I would prefer to use an external configuration of some sort to define these properties for the children. It's a bit more work, but I think in the end you will need the flexibility.
To see the extent of possible configuration settings you might look at thye "Run Configurations" settings in Eclipse. Quite a few tabs worth of configuration there.
To find the java executable that your code is currently running under (i.e. the 'path' variable in your question's sample code) there is a utility method within apache ant that can help you. You don't have to build your code with ant - just use it as a library, for this one method.
It is:
org.apache.tools.ant.util.JavaEnvUtils.getJreExecutable("java")
It takes care of the sort of special cases with different JVM vendors that others have mentioned. (And looking at the source code for it, there are more special cases than I would have imagined.)
It's in ant.jar. ant is distributed under the Apache license so hopefully you can use it how you want without hassle.
Here's a way that determines the java executable which runs the current JVM using ProcessHandle.current().info().command().
The ProcessHandle API also should allow to get the arguments. This code uses them for the new JVM if available, only replacing the current class name with another sample class. (Finding the current main class inside the arguments gets harder if you don't know its name, but in this demo it's simply "this" class. And maybe you want to reuse the same JVM options or some of them, but not the program arguments.)
However, for me (openjdk version 11.0.2, Windows 10), the ProcessInfo.arguments() is empty, so the fallback else path gets executed.
package test;
import java.lang.ProcessBuilder.Redirect;
import java.lang.management.ManagementFactory;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TestStartJvm {
public static void main(String[] args) throws Exception {
ProcessHandle.Info currentProcessInfo = ProcessHandle.current().info();
List<String> newProcessCommandLine = new LinkedList<>();
newProcessCommandLine.add(currentProcessInfo.command().get());
Optional<String[]> currentProcessArgs = currentProcessInfo.arguments();
if (currentProcessArgs.isPresent()) { // I know about orElse, but sometimes isPresent + get is handy
for (String arg: currentProcessArgs.get()) {
newProcessCommandLine.add(TestStartJvm.class.getName().equals(arg) ? TargetMain.class.getName() : arg);
}
} else {
System.err.println("don't know all process arguments, falling back to passed args array");
newProcessCommandLine.add("-classpath");
newProcessCommandLine.add(ManagementFactory.getRuntimeMXBean().getClassPath());
newProcessCommandLine.add(TargetMain.class.getName());
newProcessCommandLine.addAll(List.of(args));
}
ProcessBuilder newProcessBuilder = new ProcessBuilder(newProcessCommandLine).redirectOutput(Redirect.INHERIT)
.redirectError(Redirect.INHERIT);
Process newProcess = newProcessBuilder.start();
System.out.format("%s: process %s started%n", TestStartJvm.class.getName(), newProcessBuilder.command());
System.out.format("process exited with status %s%n", newProcess.waitFor());
}
static class TargetMain {
public static void main(String[] args) {
System.out.format("in %s: PID %s, args: %s%n", TargetMain.class.getName(), ProcessHandle.current().pid(),
Stream.of(args).collect(Collectors.joining(", ")));
}
}
}
Before ProcessHandle was added in Java 9, I did something like this to query the current JVM's command-line:
Let the user pass or configure a "PID to command-line" command template; under Windows, this could be wmic process where 'processid=%s' get commandline /format:list.
Determine PID using java.lang.management.ManagementFactory.getRuntimeMXBean().getPid().
Expand command template; execute; parse its output.

Categories