Thursday, November 19, 2009

Setting environment variables in Java

Have you ever noticed that the Java System class doesn't let you set environment variables? You can retrieve them using getenv() but there is no equivalent setenv() function.
First off, what is the environment? The manual entry for environ(7) describes the environment as:
an array of strings [that] have the form name=value. Common examples are USER (the name of the logged-in user), HOME (A user's login directory) and PATH (the sequence of directory prefixes that many programs use to search for a file known by an incomplete pathname).
It turns out that when you start up the JVM, it copies this environment into its own Map of Strings. The actual container it uses is an unmodifiable map, probably to be extra safe.
So in a running Java application we have 2 environments: the JVM copy that you can read via System.getenv() and the underlying environment that lives in the C library.
If we want to change the JVM's copy, we can do so using reflection. Or at least we should be able to, just as long as our code is not running in a sandbox. In that case you'd be right out of luck. Anyway, the code to fetch a modifiable copy of the environment could look like this:
import java.lang.reflect.Field; import java.util.Map; import java.util.HashMap; public class Environment { @SuppressWarnings("unchecked") public static Map<String, String> getenv() { try { Map<String, String> unomdifiable = System.getenv(); Class<?> cu = unomdifiable.getClass(); Field m = cu.getDeclaredField("m"); m.setAccessible(true); return (Map<String, String>)m.get(unomdifiable); } catch (Exception e) { } return new HashMap<String, String>(); } }
Calling System.getenv() returns us an UnmodifiableMap of Strings containing the copy of the C environment. In order to get to its squishy modifiable heart, we access the m member variable which is modifiable. This is because UnmodifiableMap uses the proxy pattern - it implements the Map interface, but delegates all calls for retrieving values off to a member variable Map that contains the values and does all the real work. The set methods throw UnsupportedOperation exceptions. By accessing that m we can change the JVM's copy of the environment from under its very nose. Heh heh.
Not so fast. On Windows this is slightly different. The environment there contains a different Map that allows you to search for variables in a case-insensitive way. System.getenv() returns one Map (the unmodifiable one, same as Linux) whereas System.getenv("value") looks up the value in the case-insensitive Map. In order to create a robust update-env implementation we should update both of these Maps. This needs some more reflection to get the Map in question out of the ProcessEnvironment, probably like this:
@SuppressWarnings("unchecked") public static Map<String, String> getwinenv() { try { Class<?> sc = Class.forName("java.lang.ProcessEnvironment"); Field caseinsensitive = sc.getDeclaredField("theCaseInsensitiveEnvironment"); caseinsensitive.setAccessible(true); return (Map<String, String>)caseinsensitive.get(null); } catch (Exception e) { } return new HashMap<String, String>(); }
An example of how this may be useful without going any further would be if you have the DISPLAY environment variable set but you cannot use it (for example you connect via ssh -X, start a background process, then disconnect closing the X session connection too). In this situation even if you tell java.awt to run headless it may see that DISPLAY is set, try to use it and throw an exception. By clearing DISPLAY we can use headless methods to create off-screen graphics, to send them to a printer or to save a fake screen shot to file maybe for automated testing.
However this is not enough to affect the environment for any child processes. They will still only see the original, unmodified environment that the JVM craftily made a copy of at start-up. The comment in java.lang.ProcessEnvironment says it all:
// We cache the C environment.  This means that subsequent calls
// to putenv/setenv from C will not be visible from Java code.
Grrrr! By far the easiest approach here is to just use the ProcessBuilder. This lets you change the environment before launching the child process. Win. End of post.
No! I really want to change the underlying C environment for no good reason!
In order to change that we have to resort to either JNI or JNA. Lets start with JNA, it is the easier of the two and needs less tools. Just download a jar file and with the Java Compiler you're good to hack.
In case you have not heard of it, JNA is to Java what ctypes is to Python - a byte-code library that lets you open native, compiled, shared libraries and call the C routines within directly from Java code. JNI on the other hand requires you to create C++ functions, compile these, then call them from Java.
Armed with the JNA classes we can wrap the standard setenv() and unsetenv() functions from the C library.
import com.sun.jna.Library; import com.sun.jna.Native; public class Environment { public interface LibC extends Library { public int setenv(String name, String value, int overwrite); public int unsetenv(String name); } static LibC libc = (LibC) Native.loadLibrary("c", LibC.class); }
That works fine on Linux, but on Windows we have to take a different approach. There neither setenv nor unsetenv exist, instead we have to call _putenv. This function accepts a "name=value" string, and if we pass "name=" we can delete a variable from the environment. Unfortunately this multi-platform approach messes up the code a fair amount. Here is one way to do it:
// Inside the Environment class... public interface WinLibC extends Library { public int _putenv(String name); } public interface LinuxLibC extends Library { public int setenv(String name, String value, int overwrite); public int unsetenv(String name); } static public class POSIX { static Object libc; static { if (System.getProperty("os.name").equals("Linux")) { libc = Native.loadLibrary("c", LinuxLibC.class); } else { libc = Native.loadLibrary("msvcrt", WinLibC.class); } } public int setenv(String name, String value, int overwrite) { if (libc instanceof LinuxLibC) { return ((LinuxLibC)libc).setenv(name, value, overwrite); } else { return ((WinLibC)libc)._putenv(name + "=" + value); } } public int unsetenv(String name) { if (libc instanceof LinuxLibC) { return ((LinuxLibC)libc).unsetenv(name); } else { return ((WinLibC)libc)._putenv(name + "="); } } } static POSIX libc = new POSIX();
Here we use JNA to load either libc on Linux or the msvcrt DLL (which contains _putenv) on Windows. There are ugly casts in there, and other OSes are left as an exercise to the reader, but this means that I can call POSIX.setenv() or unsetenv() and have it work.
To complete the picture, the JNI equivalent of this would be:
public class Environment { public static class LibC { public native int setenv(String name, String value, int overwrite); public native int unsetenv(String name); LibC() { System.loadLibrary("Environment_LibC"); } } static LibC libc = new LibC(); }
The call to System.loadLibrary() loads a dynamic/shared library. On Linux it looks for "libEnvironment_LibC.so" and on Windows "Environment_LibC.dll". The implementation of those native calls could be like this C++ code:
#include "Environment_LibC.h" #include <stdlib.h> #ifdef WINDOWS #include <string> #endif struct JavaString { JavaString(JNIEnv *env, jstring val): m_env(env), m_val(val), m_ptr(env->GetStringUTFChars(val, 0)) {} ~JavaString() { m_env->ReleaseStringUTFChars(m_val, m_ptr); } operator const char*() const { return m_ptr; } JNIEnv *m_env; jstring &m_val; const char *m_ptr; }; JNIEXPORT jint JNICALL Java_Environment_00024LibC_setenv (JNIEnv *env, jobject obj, jstring name, jstring value, jint overwrite) { JavaString namep(env, name); JavaString valuep(env, value); #ifdef WINDOWS std::string s(namep); s += "="; s += valuep; int res = _putenv(s.c_str()); #else int res = setenv(namep, valuep, overwrite); #endif return res; } JNIEXPORT jint JNICALL Java_Environment_00024LibC_unsetenv (JNIEnv *env, jobject obj, jstring name) { JavaString namep(env, name); #ifdef WINDOWS std::string s(namep); s += "="; int res = _putenv(s.c_str()); #else int res = unsetenv(namep); #endif return res; }
You generate the header files using javah - that gives you the strange function names needed - and compile the code as C++ to produce a shared library:
javac Environment.java
javah Environment
g++ -shared Environment_LibC.cc -o libEnvironment_LibC.so -I$(JAVA_HOME)/include/linux -I$(JAVA_HOME)/include/
This library needs to be in one of the usual places to work - somewhere where it can be found by dlopen(). So either in /usr/lib, a directory where ldconfig looks, or in one of the paths in LD_LIBRARY_PATH. This depends on the OS you are using. For Windows, Solaris or Mac you need a whole different set of flags and incantations. You can see why I lean towards JNA, even though it has its own problems. For the record this is how to compile the JNI library on Windows using MinGW:
g++ -DWINDOWS -Wl,--kill-at -shared Environment_LibC.cc -o Environment_LibC.dll -I$(JAVA_HOME)/include/win32 -I$(JAVA_HOME)/include
That --kill-at switch is a real gotcha. Without it the function symbol that the MinGW compiler produces is not the one that the JVM was expecting. On Windows the library itself must be in the current directory, or in one of the directories listed in the PATH variable.
As you can see we repeat the whole setenv-and-unsetenv-do-not-exist dance and use _putenv() for both. Here I fudge it with a bit of ifdeffing. Meh.
Now that we have a way to call the C library's setenv() and unsetenv() (or equivalent), let's wrap it all up. Here are the final setenv() and unsetenv() functions that update the C environment and the Java one too:
// inside the Environment class... public static int unsetenv(String name) { Map<String, String> map = getenv(); map.remove(name); Map<String, String> env2 = getwinenv(); env2.remove(name); return libc.unsetenv(name); } public static int setenv(String name, String value, boolean overwrite) { if (name.lastIndexOf("=") != -1) { throw new IllegalArgumentException( "Environment variable cannot contain '='"); } Map<String, String> map = getenv(); boolean contains = map.containsKey(name); if (!contains || overwrite) { map.put(name, value); Map<String, String> env2 = getwinenv(); env2.put(name, value); } return libc.setenv(name, value, overwrite?1:0); }
Curiously enough ProcessEnvironment is wired so as to validate the values that you add to the "unmodifiable" Map, but the case insensitive equivalent on Windows is not validated. If you try and add an invalid environment variable, such as one with a name that contains =, only the unmodifiable map will throw an IllegalArgumentException. This makes it fairly robust as the nasty name doesn't trickle down to the underlying C environment, but for Windows we have to do an extra check manually.
I've uploaded a tarball with all the files mentioned on here together with the dependencies to my GBA Remakes site. So now you've no excuse to not go off setting environment variables like mad.
Of course you shouldn't really do any of this. This post was 60% "if you really need to", 40% "might be useful". ProcessBuilder is the way to go for changing the environment in child processes.
The only thing that you might want to do is change the environment in a running Java process, but even then it is probably easier to create a wrapper script or batch file that launches your program and fiddle the environment prior to launching the JVM. Using reflection to access all those inner member variables is pretty flaky - if they change their name in some future version, your code will stop working. Happy hacking :-)

8 comments:

  1. Thanks very much for putting this together, it gives me a great reference for recalling x-platform quirks with env vars, working around them in multiple languages, and evaluating whether it's worth it--all very clearly and succintly. Nice Job!

    -chris walquist

    ReplyDelete
  2. This has ALMOST got me out of a very deep hole....

    It worked very nicely for me, EXCEPT that I get lots of stack traces dumps with the following error:

    java.awt.HeadlessException:
    No X11 DISPLAY variable was set, but this program performed an operation which requires it.

    Any idea how to circumvent this?

    ReplyDelete
  3. Just a word of explanation on my previous post - I get this error when using the JNA solution to setenv

    ReplyDelete
  4. okay - and one more important point - I am running this code in a batch environment, so there is no display at all - and certainly its not running under X windows.

    ReplyDelete
  5. At a guess, looking at the code:

    http://www.google.com/codesearch/p?hl=en#ih5hvYJNSIA/src/share/classes/java/awt/HeadlessException.java&q=getheadlessmessage&exact_package=http://hg.openjdk.java.net/jdk7/l10n/jdk&sa=N&cd=2&ct=rc

    and:

    http://www.google.com/codesearch/p?hl=en#ih5hvYJNSIA/src/share/classes/java/awt/GraphicsEnvironment.java&q=%22No%20X11%20DISPLAY%20variable%20was%20set%22&sa=N&cd=1&ct=rc

    You're using some keyboard/mouse/display function when you shouldn't be. Using some AWT classes will do that sort of thing. Search for HeadlessException in google code.

    ReplyDelete
  6. This is what is so bizarre, because I'm pretty sure I'm not doing anything of the sort.

    I have your copied your code pretty much as is, and I wasn't getting the error before.

    I put in some debugging statements, and the error seems to come when I do:

    libc = Native.loadLibrary("c", LinuxLibC.class);

    so it seems to me as though there is something in the library initialization that demands it.

    I tried doing

    System.setProperty("java.awt.headless","true");

    beforehand, which some web pages seemed to suggest would help, but instead I now get:

    java.awt.HeadlessException
    java.awt.HeadlessException
    at java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:159)
    at java.awt.Window.(Window.java:431)
    at java.awt.Frame.(Frame.java:403)
    at java.awt.Frame.(Frame.java:368)
    at com.trend.iwss.jscan.runtime.BaseDialog.getActiveFrame(BaseDialog.java:75)
    at com.trend.iwss.jscan.runtime.AllowDialog.make(AllowDialog.java:32)
    at com.trend.iwss.jscan.runtime.PolicyRuntime.showAllowDialog(PolicyRuntime.java:325)
    at com.trend.iwss.jscan.runtime.PolicyRuntime.stopActionInner(PolicyRuntime.java:240)
    at com.trend.iwss.jscan.runtime.PolicyRuntime.stopAction(PolicyRuntime.java:172)
    at com.trend.iwss.jscan.runtime.MiscPolicyRuntime._preFilter(MiscPolicyRuntime.java:182)
    at com.trend.iwss.jscan.runtime.PolicyRuntime.preFilter(PolicyRuntime.java:132)
    at com.trend.iwss.jscan.runtime.MiscPolicyRuntime.preFilter(MiscPolicyRuntime.java:142)
    at com.sun.jna.Native.loadNativeLibrary(Native.java:509)
    at com.sun.jna.Native.(Native.java:91)
    at -->> returning Frame NULL
    .... my code
    -->> returning Frame NULL

    and also a completely unrequested listing:

    Current policy properties:
    mmc.sess_pe_act.block_unsigned: false
    window.num_max: 5
    jscan.sess_applet_act.sig_trusted: pass
    file.destructive.state: disabled
    jscan.sess_applet_act.block_all: false
    window.num_limited: true
    jscan.sess_applet_act.unsigned: instrument
    mmc.sess_pe_act.action: validate
    jscan.session.daemon_protocol: http
    file.read.state: disabled
    mmc.sess_pe_act.block_invalid: true
    mmc.sess_pe_act.block_blacklisted: false
    net.bind_enable: false
    jscan.session.policyname: TU1DIERlZmF1bHQgUG9saWN5
    mmc.sess_cab_act.block_unsigned: false
    file.nondestructive.state: disabled
    jscan.session.origin_uri: http://mirrors.ibiblio.org/pub/mirrors/maven2/org/jruby/extras/jna/3.0.2/jna-3.0.2.jar
    mmc.sess_cab_act.action: validate
    net.connect_other: false
    jscan.session.user_ipaddr: ...a value....
    jscan.sess_applet_act.sig_invalid: block
    mmc.sess_cab_act.block_invalid: true
    thread.thread_num_max: 8
    jscan.sess_applet_act.sig_blacklisted: block
    net.connect_src: true
    thread.thread_num_limited: true
    jscan.sess_applet_act.stub_out_blocked_applet: true
    mmc.sess_cab_act.block_blacklisted: true
    thread.threadgroup_create: false
    file.write.state: disabled
    -->> returning Frame NULL


    The code actually works as you wrote, and as I said, its getting me out of a very deep hole someone else dug for me. I'll keep plugging away and if I find something I'll post here, but obviously any other ideas would be greatly appreciated.

    ReplyDelete
  7. Looks to me like you can't use Window in a headless environment.

    http://download.oracle.com/javase/1.4.2/docs/api/java/awt/Window.html#Window%28java.awt.Frame%29

    Throws: IllegalArgumentException - if gc is not from a screen device; this exception is always thrown when GraphicsEnvironment.isHeadless returns true

    ReplyDelete
  8. Thanks a lot... It worked really well for me...

    ReplyDelete

Note: only a member of this blog may post a comment.