diff --git a/readme.md b/readme.md index 7b2a0b4..b8bb69b 100755 --- a/readme.md +++ b/readme.md @@ -16,6 +16,7 @@ These APIs support Java 5 and later. Some of these APIs overlap with APIs availa * Get the PID of the current process. * Get and set the process working directory. +* Get and set the process environment variables. ### Terminal and console diff --git a/src/main/cpp/posix.cpp b/src/main/cpp/posix.cpp index 1938753..94e31c3 100755 --- a/src/main/cpp/posix.cpp +++ b/src/main/cpp/posix.cpp @@ -168,6 +168,34 @@ Java_net_rubygrapefruit_platform_internal_jni_PosixProcessFunctions_setWorkingDi free(path); } +JNIEXPORT jstring JNICALL +Java_net_rubygrapefruit_platform_internal_jni_PosixProcessFunctions_getEnvironmentVariable(JNIEnv *env, jclass target, jstring var, jobject result) { + char* varStr = java_to_char(env, var, result); + char* valueStr = getenv(varStr); + free(varStr); + if (valueStr == NULL) { + return NULL; + } + return char_to_java(env, valueStr, result); +} + +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_PosixProcessFunctions_setEnvironmentVariable(JNIEnv *env, jclass target, jstring var, jstring value, jobject result) { + char* varStr = java_to_char(env, var, result); + if (value == NULL) { + if (setenv(varStr, "", 1) != 0) { + mark_failed_with_errno(env, "could not putenv()", result); + } + } else { + char* valueStr = java_to_char(env, value, result); + if (setenv(varStr, valueStr, 1) != 0) { + mark_failed_with_errno(env, "could not putenv()", result); + } + free(valueStr); + } + free(varStr); +} + /* * Terminal functions */ diff --git a/src/main/java/net/rubygrapefruit/platform/Process.java b/src/main/java/net/rubygrapefruit/platform/Process.java index 956fa29..dbb98a7 100755 --- a/src/main/java/net/rubygrapefruit/platform/Process.java +++ b/src/main/java/net/rubygrapefruit/platform/Process.java @@ -50,7 +50,8 @@ public interface Process extends NativeIntegration { /** * Get the value of an environment variable. * - * @return The value or null if no such environment variable. + * @return The value or null if no such environment variable. Also returns null for an environment variable whose + * value is an empty string. * @throws NativeException On failure. */ @ThreadSafe @@ -59,8 +60,9 @@ public interface Process extends NativeIntegration { /** * Sets the value of an environment variable. * - * @param value the new value. Use null to remove the environment variable. Note that on some platforms it is not - * possible to remove the environment variable. On such platforms, the value is set to an empty string instead. + * @param value the new value. Use null or an empty string to remove the environment variable. Note that on some + * platforms it is not possible to remove the environment variable safely. On such platforms, the value is set to an + * empty string instead. * @throws NativeException On failure. */ @ThreadSafe diff --git a/src/main/java/net/rubygrapefruit/platform/internal/DefaultProcess.java b/src/main/java/net/rubygrapefruit/platform/internal/DefaultProcess.java index 9eee880..f136ecf 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/DefaultProcess.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/DefaultProcess.java @@ -23,56 +23,45 @@ import net.rubygrapefruit.platform.internal.jni.PosixProcessFunctions; import java.io.File; public class DefaultProcess implements Process { - private final Object workingDirectoryLock = new Object(); - private final Object environmentLock = new Object(); - public int getProcessId() throws NativeException { return PosixProcessFunctions.getPid(); } public File getWorkingDirectory() throws NativeException { FunctionResult result = new FunctionResult(); - synchronized (workingDirectoryLock) { - String dir = PosixProcessFunctions.getWorkingDirectory(result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not get process working directory: %s", - result.getMessage())); - } - return new File(dir); + String dir = PosixProcessFunctions.getWorkingDirectory(result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not get process working directory: %s", + result.getMessage())); } + return new File(dir); } public void setWorkingDirectory(File directory) throws NativeException { FunctionResult result = new FunctionResult(); - synchronized (workingDirectoryLock) { - PosixProcessFunctions.setWorkingDirectory(directory.getAbsolutePath(), result); - if (result.isFailed()) { - throw new NativeException(String.format("Could not set process working directory: %s", - result.getMessage())); - } - System.setProperty("user.dir", directory.getAbsolutePath()); + PosixProcessFunctions.setWorkingDirectory(directory.getAbsolutePath(), result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not set process working directory to '%s': %s", + directory.getAbsoluteFile(), result.getMessage())); } } public String getEnvironmentVariable(String name) throws NativeException { FunctionResult result = new FunctionResult(); - String value; - synchronized (environmentLock) { - value = PosixProcessFunctions.getEnvironmentVariable(name, result); - } + String value = PosixProcessFunctions.getEnvironmentVariable(name, result); if (result.isFailed()) { - throw new NativeException(String.format("Could not get the value of environment variable '%s': %s", name, result.getMessage())); + throw new NativeException(String.format("Could not get the value of environment variable '%s': %s", name, + result.getMessage())); } return value; } public void setEnvironmentVariable(String name, String value) throws NativeException { FunctionResult result = new FunctionResult(); - synchronized (environmentLock) { - PosixProcessFunctions.setEnvironmentVariable(name, value, result); - } + PosixProcessFunctions.setEnvironmentVariable(name, value, result); if (result.isFailed()) { - throw new NativeException(String.format("Could not set the value of environment variable '%s': %s", name, result.getMessage())); + throw new NativeException(String.format("Could not set the value of environment variable '%s': %s", name, + result.getMessage())); } } } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/Platform.java b/src/main/java/net/rubygrapefruit/platform/internal/Platform.java index bf1463f..3eb04c6 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/Platform.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/Platform.java @@ -91,7 +91,7 @@ public abstract class Platform { @Override public T get(Class type, NativeLibraryLoader nativeLibraryLoader) { if (type.equals(Process.class)) { - return type.cast(new DefaultProcess()); + return type.cast(new WrapperProcess(new DefaultProcess())); } if (type.equals(Terminals.class)) { return type.cast(new WindowsTerminals()); @@ -129,7 +129,7 @@ public abstract class Platform { return type.cast(new DefaultPosixFile()); } if (type.equals(Process.class)) { - return type.cast(new DefaultProcess()); + return type.cast(new WrapperProcess(new DefaultProcess())); } if (type.equals(Terminals.class)) { nativeLibraryLoader.load(getCursesLibraryName()); diff --git a/src/main/java/net/rubygrapefruit/platform/internal/WrapperProcess.java b/src/main/java/net/rubygrapefruit/platform/internal/WrapperProcess.java new file mode 100644 index 0000000..f57b9df --- /dev/null +++ b/src/main/java/net/rubygrapefruit/platform/internal/WrapperProcess.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012 Adam Murdoch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.rubygrapefruit.platform.internal; + +import net.rubygrapefruit.platform.NativeException; +import net.rubygrapefruit.platform.Process; +import net.rubygrapefruit.platform.ThreadSafe; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.Map; + +/** + * A {@link Process} implementation that wraps another to add thread-safety and to update the JVM's internal view of + * various process properties. + */ +@ThreadSafe +public class WrapperProcess implements Process { + private final Process process; + private final Object workingDirectoryLock = new Object(); + private final Object environmentLock = new Object(); + + public WrapperProcess(Process process) { + this.process = process; + } + + @Override + public String toString() { + return process.toString(); + } + + public int getProcessId() throws NativeException { + return process.getProcessId(); + } + + public File getWorkingDirectory() throws NativeException { + synchronized (workingDirectoryLock) { + return process.getWorkingDirectory(); + } + } + + public void setWorkingDirectory(File directory) throws NativeException { + synchronized (workingDirectoryLock) { + process.setWorkingDirectory(directory); + System.setProperty("user.dir", directory.getAbsolutePath()); + } + } + + public String getEnvironmentVariable(String name) throws NativeException { + synchronized (environmentLock) { + String value = process.getEnvironmentVariable(name); + return value == null || value.length() == 0 ? null : value; + } + } + + public void setEnvironmentVariable(String name, String value) throws NativeException { + synchronized (environmentLock) { + if (value == null || value.length() == 0) { + process.setEnvironmentVariable(name, null); + removeEnvInternal(name); + } else { + process.setEnvironmentVariable(name, value); + setEnvInternal(name, value); + } + } + } + + private void removeEnvInternal(String name) { + getEnv().remove(name); + } + + private void setEnvInternal(String name, String value) { + getEnv().put(name, value); + } + + private Map getEnv() { + try { + Map theUnmodifiableEnvironment = System.getenv(); + Class cu = theUnmodifiableEnvironment.getClass(); + Field m = cu.getDeclaredField("m"); + m.setAccessible(true); + return (Map)m.get(theUnmodifiableEnvironment); + } catch (Exception e) { + throw new NativeException("Unable to get mutable environment map.", e); + } + } + +} diff --git a/src/test/groovy/net/rubygrapefruit/platform/ProcessTest.groovy b/src/test/groovy/net/rubygrapefruit/platform/ProcessTest.groovy index 77b2055..03b8038 100755 --- a/src/test/groovy/net/rubygrapefruit/platform/ProcessTest.groovy +++ b/src/test/groovy/net/rubygrapefruit/platform/ProcessTest.groovy @@ -19,6 +19,7 @@ package net.rubygrapefruit.platform import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification +import spock.lang.Unroll class ProcessTest extends Specification { @Rule TemporaryFolder tmpDir @@ -36,6 +37,7 @@ class ProcessTest extends Specification { def "can get and change working directory"() { def newDir = tmpDir.newFolder(dir).canonicalFile + assert newDir.directory when: def original = process.workingDirectory @@ -67,7 +69,7 @@ class ProcessTest extends Specification { then: NativeException e = thrown() - e.message.startsWith("Could not set process working directory:") + e.message.startsWith("Could not set process working directory") } def "can get and set and remove environment variable"() {