Implements JIRA feature request SGF-226 enabling Spring Data GemFire to retrieve the shared, persistent cluster configuration from a GemFire 8 Locator Shared Configuration service when the SDG configured peer cache member joins the cluster.

This commit is contained in:
John Blum
2014-07-17 20:27:20 -07:00
parent 76aa819be7
commit f427125669
24 changed files with 1771 additions and 290 deletions

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.gemfire.fork.LocatorProcess;
import org.springframework.data.gemfire.process.ProcessExecutor;
import org.springframework.data.gemfire.process.ProcessWrapper;
import org.springframework.data.gemfire.test.support.ThreadUtils;
import org.springframework.data.gemfire.test.support.ZipUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.FileSystemUtils;
import com.gemstone.gemfire.cache.DataPolicy;
import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.cache.Scope;
/**
* The CacheUsingSharedConfigurationIntegrationTest class is a test suite of test cases testing the integration of
* Spring Data GemFire with GemFire 8's new shared, persistent, cluster configuration service.
*
* @author John Blum
* @see org.junit.Test
* @see org.junit.runner.RunWith
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner
* @since 1.5.0
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("cacheUsingSharedConfigurationIntegrationTest.xml")
@SuppressWarnings("unused")
public class CacheUsingSharedConfigurationIntegrationTest {
@Resource(name = "SharedConfigRegion")
private Region<Long, String> sharedConfigRegion;
@Resource(name = "NativeLocalRegion")
private Region<Integer, String> nativeLocalRegion;
@Resource(name = "NativePartitionRegion")
private Region<Integer, String> nativePartitionRegion;
@Resource(name = "NativeReplicateRegion")
private Region<Integer, String> nativeReplicateRegion;
@Resource(name = "LocalRegion")
private Region<Integer, String> localRegion;
private static File locatorWorkingDirectory;
private static ProcessWrapper locatorProcess;
@BeforeClass
public static void testSuiteSetup() throws IOException {
String locatorName = "SharedConfigLocator";
locatorWorkingDirectory = new File(System.getProperty("user.dir"), locatorName.toLowerCase());
assertTrue(locatorWorkingDirectory.isDirectory() || locatorWorkingDirectory.mkdirs());
ZipUtils.unzip(new ClassPathResource("/shared_config.zip"), locatorWorkingDirectory);
List<String> arguments = new ArrayList<String>();
arguments.add("-Dgemfire.name=" + locatorName);
arguments.add("-Dspring.gemfire.enable-shared-configuration=true");
arguments.add("-Dspring.gemfire.load-shared-configuration=true");
locatorProcess = ProcessExecutor.launch(locatorWorkingDirectory, LocatorProcess.class,
arguments.toArray(new String[arguments.size()]));
locatorProcess.registerShutdownHook();
waitForLocatorStart(TimeUnit.SECONDS.toMillis(30));
//System.out.println("Shared Configuration Locator should be running!");
}
private static void waitForLocatorStart(final long milliseconds) {
ThreadUtils.timedWait(milliseconds, 500, new ThreadUtils.WaitCondition() {
File pidControlFile = new File(locatorWorkingDirectory, LocatorProcess.getLocatorProcessControlFilename());
@Override public boolean waiting() {
return !pidControlFile.isFile();
}
});
}
@AfterClass
public static void testSuiteTearDown() {
locatorProcess.shutdown();
FileSystemUtils.deleteRecursively(locatorWorkingDirectory);
}
protected Region assertRegion(final Region actualRegion, final String expectedRegionName) {
return assertRegion(actualRegion, expectedRegionName, Region.SEPARATOR+expectedRegionName);
}
protected Region assertRegion(final Region actualRegion, final String expectedRegionName, final String expectedRegionFullPath) {
assertNotNull(String.format("The '%1$s' was not properly configured and initialized!", expectedRegionName), actualRegion);
assertEquals(expectedRegionName, actualRegion.getName());
assertEquals(expectedRegionFullPath, actualRegion.getFullPath());
return actualRegion;
}
protected Region assertRegionAttributes(final Region actualRegion, final DataPolicy expectedDataPolicy, final Scope expectedScope) {
assertNotNull(actualRegion);
assertNotNull(actualRegion.getAttributes());
assertEquals(expectedDataPolicy, actualRegion.getAttributes().getDataPolicy());
assertEquals(expectedScope, actualRegion.getAttributes().getScope());
return actualRegion;
}
@Test
public void testConfiguration() {
assertRegionAttributes(assertRegion(sharedConfigRegion, "SharedConfigRegion"),
DataPolicy.PARTITION, Scope.DISTRIBUTED_NO_ACK);
assertRegionAttributes(assertRegion(nativeLocalRegion, "NativeLocalRegion"), DataPolicy.NORMAL, Scope.LOCAL);
assertRegionAttributes(assertRegion(nativePartitionRegion, "NativePartitionRegion"),
DataPolicy.PARTITION, Scope.DISTRIBUTED_NO_ACK);
assertRegionAttributes(assertRegion(nativeReplicateRegion, "NativeReplicateRegion"),
DataPolicy.REPLICATE, Scope.DISTRIBUTED_ACK);
assertRegionAttributes(assertRegion(localRegion, "LocalRegion"), DataPolicy.NORMAL, Scope.LOCAL);
}
}

View File

@@ -27,6 +27,9 @@ import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.data.gemfire.fork.CacheServerProcess;
import org.springframework.data.gemfire.test.support.IOUtils;
import org.springframework.data.gemfire.test.support.ThreadUtils;
import org.springframework.util.StringUtils;
/**
@@ -34,7 +37,9 @@ import org.springframework.util.StringUtils;
*
* @author Costin Leau
* @author John Blum
* @deprecated ForkUtils has serious design flaws; please use ProcessExecutor instead
*/
@Deprecated
public class ForkUtil {
private static OutputStream processStandardInStream;
@@ -47,13 +52,11 @@ public class ForkUtil {
private static OutputStream cloneJVM(final String arguments) {
String[] args = arguments.split("\\s+");
List<String> command = new ArrayList<String>(args.length + 5);
List<String> command = new ArrayList<String>(args.length + 3);
command.add(JAVA_EXE);
command.add("-classpath");
command.add(JAVA_CLASSPATH);
//command.add("-Xms256m");
//command.add("-Xmx512m");
command.addAll(Arrays.asList(args));
Process javaProcess;
@@ -72,8 +75,8 @@ public class ForkUtil {
final AtomicBoolean runCondition = new AtomicBoolean(true);
final Process p = javaProcess;
new Thread(newProcessStreamReaderRunnable(runCondition, p.getErrorStream(), "[FORK-ERROR]")).start();
new Thread(newProcessStreamReaderRunnable(runCondition, p.getInputStream(), "[FORK]")).start();
startNewThread(newProcessStreamReader(runCondition, p.getErrorStream(), "[FORK-ERROR]"));
startNewThread(newProcessStreamReader(runCondition, p.getInputStream(), "[FORK]"));
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
@@ -98,61 +101,69 @@ public class ForkUtil {
return processStandardInStream;
}
protected static Runnable newProcessStreamReaderRunnable(final AtomicBoolean runCondition, final InputStream processStream, final String label) {
final BufferedReader processStreamReader = new BufferedReader(new InputStreamReader(processStream));
protected static Thread startNewThread(final Runnable runnable) {
Thread runnableThread = new Thread(runnable);
runnableThread.start();
return runnableThread;
}
protected static Runnable newProcessStreamReader(final AtomicBoolean runCondition, final InputStream processStream, final String label) {
return new Runnable() {
public void run() {
BufferedReader processStreamReader = new BufferedReader(new InputStreamReader(processStream));
try {
do {
while (runCondition.get()) {
for (String line = "Reading..."; line != null; line = processStreamReader.readLine()) {
System.out.printf("%1$s %2$s%n ", label, line);
}
Thread.sleep(200);
}
while (runCondition.get());
}
catch (Exception ignore) {
}
finally {
IOUtils.close(processStreamReader);
}
}
};
}
public static OutputStream cacheServer(Class<?> clazz) {
return startCacheServer(clazz.getName());
}
public static OutputStream cacheServer() {
return startCacheServer("org.springframework.data.gemfire.fork.CacheServerProcess");
return cacheServer(CacheServerProcess.class);
}
public static OutputStream cacheServer(Class<?> type) {
return startCacheServer(type.getName());
}
public static OutputStream startCacheServer(String args) {
return startGemFireProcess(args, "cache server");
}
protected static OutputStream startGemFireProcess(final String args, final String processName) {
String className = args.split(" ")[0];
System.out.println("main class:" + className);
System.out.println("main class: " + className);
if (controlFileExists(className)) {
deleteControlFile(className);
}
OutputStream os = cloneJVM(args);
int maxTime = 30000;
int time = 0;
while (!controlFileExists(className) && time < maxTime) {
try {
Thread.sleep(500);
time += 500;
} catch (InterruptedException ex) {
// ignore and move on
}
OutputStream outputStream = cloneJVM(args);
final long timeout = System.currentTimeMillis() + 30000;
while (!controlFileExists(className) && System.currentTimeMillis() < timeout) {
ThreadUtils.sleep(500);
}
if (controlFileExists(className)) {
System.out.println("[FORK] Started cache server");
System.out.printf("[FORK] Started %1$s%n", processName);
}
else {
throw new RuntimeException("could not fork cache server");
throw new RuntimeException(String.format("Failed to fork %1$s", processName));
}
return os;
return outputStream;
}
public static void sendSignal() {
@@ -165,19 +176,16 @@ public class ForkUtil {
}
}
public static boolean deleteControlFile(String name) {
String path = TEMP_DIRECTORY + File.separator + name;
return new File(path).delete();
public static boolean createControlFile(final String name) throws IOException {
return new File(TEMP_DIRECTORY + File.separator + name).createNewFile();
}
public static boolean createControlFile(String name) throws IOException {
String path = TEMP_DIRECTORY + File.separator + name;
return new File(path).createNewFile();
public static boolean controlFileExists(final String name) {
return new File(TEMP_DIRECTORY + File.separator + name).isFile();
}
public static boolean controlFileExists(String name) {
String path = TEMP_DIRECTORY + File.separator + name;
return new File(path).exists();
public static boolean deleteControlFile(final String name) {
return new File(TEMP_DIRECTORY + File.separator + name).delete();
}
}

View File

@@ -74,7 +74,7 @@ public class GemfireV7GatewayNamespaceTest extends RecreatingContextTest {
@Before
@Override
public void createCtx() {
if (GemfireUtils.GEMFIRE_VERSION.startsWith("7")) {
if (GemfireUtils.isGemfireVersion7OrAbove()) {
super.createCtx();
}
}

View File

@@ -38,12 +38,12 @@ import com.gemstone.gemfire.cache.server.CacheServer;
public class CacheServerProcess {
public static void main(final String[] args) throws Exception {
Properties props = new Properties();
props.setProperty("name", "CqServer");
props.setProperty("mcast-port", "0");
props.setProperty("log-level", "warning");
Properties gemfireProperties = new Properties();
gemfireProperties.setProperty("name", "CqServer");
gemfireProperties.setProperty("mcast-port", "0");
gemfireProperties.setProperty("log-level", "warning");
Cache cache = new CacheFactory(props).create();
Cache cache = new CacheFactory(gemfireProperties).create();
RegionFactory<String, Integer> regionFactory = cache.createRegionFactory();
regionFactory.setDataPolicy(DataPolicy.REPLICATE);
@@ -73,6 +73,8 @@ public class CacheServerProcess {
System.out.println("Waiting for shutdown...");
bufferedReader.readLine();
System.out.println("Shutting down!");
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.fork;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.springframework.data.gemfire.process.support.ProcessUtils;
import org.springframework.data.gemfire.test.support.ThreadUtils;
import com.gemstone.gemfire.distributed.LocatorLauncher;
import com.gemstone.gemfire.distributed.internal.DistributionConfig;
import com.gemstone.gemfire.distributed.internal.InternalLocator;
import com.gemstone.gemfire.distributed.internal.SharedConfiguration;
/**
* The LocatorProcess class is a main Java class that is used fork and launch a GemFire Locator process using the
* LocatorLauncher class.
*
* @author John Blum
* @see com.gemstone.gemfire.distributed.LocatorLauncher
* @since 1.5.0
*/
public class LocatorProcess {
public static final int DEFAULT_LOCATOR_PORT = 20668;
public static final String DEFAULT_GEMFIRE_MEMBER_NAME = "SpringDataGemFire-Locator";
public static final String DEFAULT_HOSTNAME_FOR_CLIENTS = "localhost";
public static final String DEFAULT_HTTP_SERVICE_PORT = "0";
public static final String DEFAULT_LOG_LEVEL = "config";
public static void main(final String... args) throws IOException {
LocatorLauncher locatorLauncher = buildLocatorLauncher();
registerShutdownHook();
// start the GemFire Locator process...
locatorLauncher.start();
waitForLocatorStart(TimeUnit.SECONDS.toMillis(20));
ProcessUtils.writePid(new File(System.getProperty("user.dir"), getLocatorProcessControlFilename()),
ProcessUtils.currentPid());
ProcessUtils.waitForStopSignal();
}
public static String getLocatorProcessControlFilename() {
return LocatorProcess.class.getSimpleName().toLowerCase().concat(".pid");
}
private static LocatorLauncher buildLocatorLauncher() {
return new LocatorLauncher.Builder()
.setMemberName(DEFAULT_GEMFIRE_MEMBER_NAME)
.setHostnameForClients(System.getProperty("spring.gemfire.hostname-for-clients",
DEFAULT_HOSTNAME_FOR_CLIENTS))
.setPort(Integer.getInteger("spring.gemfire.locator-port", DEFAULT_LOCATOR_PORT))
.setRedirectOutput(false)
.set(DistributionConfig.ENABLE_SHARED_CONFIGURATION_NAME, String.valueOf(Boolean.getBoolean(
"spring.gemfire.enable-shared-configuration")))
.set(DistributionConfig.HTTP_SERVICE_PORT_NAME, System.getProperty("spring.gemfire.http-service-port",
DEFAULT_HTTP_SERVICE_PORT))
.set(DistributionConfig.JMX_MANAGER_NAME, String.valueOf(Boolean.TRUE))
.set(DistributionConfig.JMX_MANAGER_START_NAME, String.valueOf(Boolean.FALSE))
.set(DistributionConfig.LOAD_SHARED_CONFIG_FROM_DIR_NAME, String.valueOf(Boolean.getBoolean(
"spring.gemfire.load-shared-configuration")))
.set(DistributionConfig.LOG_LEVEL_NAME, System.getProperty("spring.gemfire.log-level", DEFAULT_LOG_LEVEL))
.build();
}
private static boolean isSharedConfigurationEnabled(final InternalLocator locator) {
return (locator != null && Boolean.valueOf(locator.getDistributedSystem().getProperties().getProperty(
DistributionConfig.ENABLE_SHARED_CONFIGURATION_NAME)));
}
private static void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override public void run() {
stopSharedConfigurationService();
LocatorLauncher.getInstance().stop();
}
private void stopSharedConfigurationService() {
InternalLocator locator = InternalLocator.getLocator();
if (isSharedConfigurationEnabled(locator)) {
SharedConfiguration sharedConfiguration = locator.getSharedConfiguration();
if (sharedConfiguration != null) {
sharedConfiguration.destroySharedConfiguration();
}
}
}
}));
}
private static void waitForLocatorStart(final long milliseconds) {
final InternalLocator locator = InternalLocator.getLocator();
if (isSharedConfigurationEnabled(locator)) {
ThreadUtils.timedWait(milliseconds, 500, new ThreadUtils.WaitCondition() {
@Override public boolean waiting() {
return !locator.isSharedConfigurationRunning();
}
});
}
else {
LocatorLauncher.getInstance().waitOnStatusResponse(milliseconds, Math.min(500, milliseconds),
TimeUnit.MILLISECONDS);
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.process;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* The ProcessContext class is a container encapsulating configuration and context meta-data for a running process.
*
* @author John Blum
* @see java.lang.ProcessBuilder
* @since 1.5.0
*/
@SuppressWarnings("unused")
public class ProcessConfiguration {
private final boolean redirectingErrorStream;
private final File workingDirectory;
private final List<String> command;
private final Map<String, String> environment;
public static ProcessConfiguration create(final ProcessBuilder processBuilder) {
Assert.notNull(processBuilder, "The ProcessBuilder used to construct, configure and start the Process must not be null!");
return new ProcessConfiguration(processBuilder.command(), processBuilder.directory(),
processBuilder.environment(), processBuilder.redirectErrorStream());
}
public ProcessConfiguration(final List<String> command, final File workingDirectory,
final Map<String, String> environment, final boolean redirectingErrorStream) {
Assert.notEmpty(command, "The command used to run the process must be specified!");
Assert.isTrue((workingDirectory != null && workingDirectory.isDirectory()), String.format(
"The process working directory (%1$s) is not valid!", workingDirectory));
this.command = new ArrayList<String>(command);
this.workingDirectory = workingDirectory;
this.redirectingErrorStream = redirectingErrorStream;
this.environment = (environment != null
? Collections.unmodifiableMap(new HashMap<String, String>(environment))
: Collections.<String, String>emptyMap());
}
public List<String> getCommand() {
return Collections.unmodifiableList(command);
}
public String getCommandString() {
return StringUtils.arrayToDelimitedString(getCommand().toArray(), " ");
}
public Map<String, String> getEnvironment() {
return environment;
}
public boolean isRedirectingErrorStream() {
return redirectingErrorStream;
}
public File getWorkingDirectory() {
return workingDirectory;
}
@Override
public String toString() {
return "{ command = ".concat(getCommandString())
.concat(", workingDirectory = ".concat(getWorkingDirectory().getAbsolutePath()))
.concat(", redirectingErrorStream = ".concat(String.valueOf(isRedirectingErrorStream())))
.concat(", environment = ".concat(String.valueOf(getEnvironment())))
.concat(" }");
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.process;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* The ProcessLauncher class is a utility class for launching Java processes.
*
* @author John Blum
* @see java.lang.Process
* @see java.lang.ProcessBuilder
* @see org.springframework.data.gemfire.process.ProcessConfiguration
* @see org.springframework.data.gemfire.process.ProcessWrapper
* @since 1.5.0
*/
@SuppressWarnings("unused")
public abstract class ProcessExecutor {
protected static final String JAVA_CLASSPATH = System.getProperty("java.class.path");
protected static final String JAVA_HOME = System.getProperty("java.home");
protected static final String JAVA_EXE = JAVA_HOME.concat(File.separator).concat("bin")
.concat(File.separator).concat("java");
protected static final String USER_HOME = System.getProperty("user.home");
protected static final String USER_WORKING_DIRECTORY = System.getProperty("user.dir");
public static ProcessWrapper launch(final Class<?> type, final String... args) throws IOException {
return launch(new File(USER_WORKING_DIRECTORY), type, args);
}
public static ProcessWrapper launch(final File workingDirectory, final Class<?> type, final String... args)
throws IOException
{
ProcessBuilder processBuilder = new ProcessBuilder()
.command(buildCommand(type, args))
.directory(validateDirectory(workingDirectory))
.redirectErrorStream(true);
Process process = processBuilder.start();
ProcessWrapper processWrapper = new ProcessWrapper(process, ProcessConfiguration.create(processBuilder));
processWrapper.register(new ProcessInputStreamListener() {
@Override public void onInput(final String input) {
System.err.printf("[FORK-OUT] - %1$s%n", input);
}
});
return processWrapper;
}
protected static String[] buildCommand(final Class<?> type, final String... args) {
Assert.notNull(type != null, "The main Class to launch must not be null!");
List<String> command = new ArrayList<String>();
List<String> programArgs = Collections.emptyList();
command.add(JAVA_EXE);
command.add("-server");
command.add("-classpath");
command.add(JAVA_CLASSPATH);
if (args != null) {
programArgs = new ArrayList<String>(args.length);
for (String arg : args) {
if (isJvmOption(arg)) {
command.add(arg);
}
else if (!StringUtils.isEmpty(arg)) {
programArgs.add(arg);
}
}
}
command.add(type.getName());
command.addAll(programArgs);
return command.toArray(new String[command.size()]);
}
protected static boolean isJvmOption(final String option) {
return (!StringUtils.isEmpty(option) && (option.startsWith("-D") || option.startsWith("-X")));
}
protected static File validateDirectory(final File workingDirectory) {
Assert.isTrue(workingDirectory != null && (workingDirectory.isDirectory() || workingDirectory.mkdirs()));
return workingDirectory;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.process;
import java.util.EventListener;
/**
* The ProcessStreamListener interface is a callback listener that gets called when input arrives from either a
* process's standard output steam or standard error stream.
*
* @author John Blum
* @see java.util.EventListener
* @since 1.5.0
*/
public interface ProcessInputStreamListener extends EventListener {
void onInput(String input);
}

View File

@@ -0,0 +1,274 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.process;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.data.gemfire.process.support.PidUnavailableException;
import org.springframework.data.gemfire.process.support.ProcessUtils;
import org.springframework.data.gemfire.test.support.IOUtils;
import org.springframework.data.gemfire.test.support.ThreadUtils;
import org.springframework.data.gemfire.test.support.ThrowableUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* The ProcessWrapper class is a wrapper for a Process object representing an OS process and the ProcessBuilder used
* to construct and start the process.
*
* @author John Blum
* @see java.lang.Process
* @see java.lang.ProcessBuilder
* @since 1.5.0
*/
@SuppressWarnings("unused")
public class ProcessWrapper {
protected static final boolean DEFAULT_DAEMON_THREAD = true;
protected static final long DEFAULT_WAIT_TIME_MILLISECONDS = TimeUnit.SECONDS.toMillis(15);
private final List<ProcessInputStreamListener> listeners = new CopyOnWriteArrayList<ProcessInputStreamListener>();
protected final Logger log = Logger.getLogger(getClass().getName());
private final Process process;
private final ProcessConfiguration processConfiguration;
public ProcessWrapper(final Process process, final ProcessConfiguration processConfiguration) {
Assert.notNull(process, "The Process object backing this wrapper must not be null!");
Assert.notNull(processConfiguration, "The context and configuration meta-data providing details about"
+ " the environment in which the process is running and how the process was configured must not be null!");
this.process = process;
this.processConfiguration = processConfiguration;
postInit();
}
private void postInit() {
newThread("Process OUT Stream Reader", newProcessInputStreamReader(process.getInputStream())).start();
if (!isRedirectingErrorStream()) {
newThread("Process ERR Stream Reader", newProcessInputStreamReader(process.getErrorStream())).start();
}
}
protected Runnable newProcessInputStreamReader(final InputStream in) {
return new Runnable() {
@Override public void run() {
if (isRunning()) {
BufferedReader inputReader = new BufferedReader(new InputStreamReader(in));
try {
for (String input = inputReader.readLine(); input != null; input = inputReader.readLine()) {
for (ProcessInputStreamListener listener : listeners) {
listener.onInput(input);
}
}
}
catch (IOException ignore) {
// ignore IO error and just stop reading from the process input stream
// IO error occurred most likely because the process was terminated
}
finally {
IOUtils.close(inputReader);
}
}
}
};
}
protected Thread newThread(final String name, final Runnable task) {
Assert.isTrue(!StringUtils.isEmpty(name), "The name of the Thread must be specified!");
Assert.notNull(task, "The Thread task must not be null!");
Thread thread = new Thread(task, name);
thread.setDaemon(DEFAULT_DAEMON_THREAD);
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
public List<String> getCommand() {
return processConfiguration.getCommand();
}
public String getCommandString() {
return processConfiguration.getCommandString();
}
public Map<String, String> getEnvironment() {
return processConfiguration.getEnvironment();
}
public int getPid() {
return ProcessUtils.findAndReadPid(getWorkingDirectory());
}
public int safeGetPid() {
try {
return getPid();
}
catch (PidUnavailableException ignore) {
return -1;
}
}
public boolean isRedirectingErrorStream() {
return processConfiguration.isRedirectingErrorStream();
}
public boolean isRunning() {
return ProcessUtils.isRunning(this.process);
}
public File getWorkingDirectory() {
return processConfiguration.getWorkingDirectory();
}
public int exitValue() {
return process.exitValue();
}
public int safeExitValue() {
try {
return exitValue();
}
catch (IllegalThreadStateException ignore) {
return -1;
}
}
public boolean register(final ProcessInputStreamListener listener) {
return (listener != null && listeners.add(listener));
}
public void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override public void run() {
shutdown();
}
}));
}
public void signalStop() {
try {
ProcessUtils.signalStop(this.process);
}
catch (IOException e) {
log.warning("Failed to signal the process to stop!");
if (log.isLoggable(Level.FINE)) {
log.fine(ThrowableUtils.toString(e));
}
}
}
public int stop() {
return stop(DEFAULT_WAIT_TIME_MILLISECONDS);
}
public int stop(final long milliseconds) {
if (isRunning()) {
int exitValue = -1;
final int pid = safeGetPid();
final long timeout = (System.currentTimeMillis() + milliseconds);
final AtomicBoolean exited = new AtomicBoolean(false);
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
Future<Integer> futureExitValue = executorService.submit(new Callable<Integer>() {
@Override public Integer call() throws Exception {
process.destroy();
int exitValue = process.waitFor();
exited.set(true);
return exitValue;
}
});
while (!exited.get() && System.currentTimeMillis() < timeout) {
try {
exitValue = futureExitValue.get(milliseconds, TimeUnit.MILLISECONDS);
log.info(String.format("Process [%1$s] has been stopped.%n", pid));
}
catch (InterruptedException ignore) {
}
}
}
catch (TimeoutException e) {
exitValue = -1;
log.warning(String.format("Process [%1$d] did not stop within the allotted timeout of %2$d seconds.%n",
pid, TimeUnit.MILLISECONDS.toSeconds(milliseconds)));
}
catch (Exception ignore) {
// handles CancellationException, ExecutionException
}
finally {
executorService.shutdownNow();
}
return exitValue;
}
else {
return exitValue();
}
}
public int shutdown() {
if (isRunning()) {
log.info(String.format("Stopping process [%1$d]...%n", safeGetPid()));
signalStop();
waitFor();
}
return stop();
}
public boolean unregister(final ProcessInputStreamListener listener) {
return listeners.remove(listener);
}
public void waitFor() {
waitFor(DEFAULT_WAIT_TIME_MILLISECONDS);
}
public void waitFor(final long milliseconds) {
ThreadUtils.timedWait(milliseconds, 500, new ThreadUtils.WaitCondition() {
@Override public boolean waiting() {
return isRunning();
}
});
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.process.support;
/**
* The PidUnavailableException class is a RuntimeException indicating that the process ID (PID) is unobtainable for
* the current process.
*
* @author John Blum
* @see java.lang.RuntimeException
* @since 1.5.0
*/
@SuppressWarnings("unused")
public class PidUnavailableException extends RuntimeException {
public PidUnavailableException() {
}
public PidUnavailableException(final String message) {
super(message);
}
public PidUnavailableException(final Throwable cause) {
super(cause);
}
public PidUnavailableException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,211 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.process.support;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.Scanner;
import java.util.logging.Logger;
import org.springframework.data.gemfire.test.support.IOUtils;
import org.springframework.util.StringUtils;
import com.gemstone.gemfire.management.internal.cli.util.spring.Assert;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
/**
* The ProcessUtils class is a utilty class for working with process, or specifically instances
* of the Java Process class.
*
* @author John Blum
* @see java.lang.Process
* @see java.lang.management.RuntimeMXBean
* @see com.sun.tools.attach.VirtualMachine
* @since 1.5.0
*/
@SuppressWarnings("unused")
public abstract class ProcessUtils {
protected static final Logger log = Logger.getLogger(ProcessUtils.class.getName());
protected static final String TERM_TOKEN = "<TERM/>";
public static int currentPid() {
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
String runtimeMXBeanName = runtimeMXBean.getName();
Exception cause = null;
if (StringUtils.hasText(runtimeMXBeanName)) {
int atSignIndex = runtimeMXBeanName.indexOf('@');
if (atSignIndex > 0) {
try {
return Integer.parseInt(runtimeMXBeanName.substring(0, atSignIndex));
}
catch (NumberFormatException e) {
cause = e;
}
}
}
throw new PidUnavailableException(String.format("The process ID (PID) is not available (%1$s)!",
runtimeMXBeanName), cause);
}
public static boolean isRunning(final int processId) {
for (VirtualMachineDescriptor vmDescriptor : VirtualMachine.list()) {
if (String.valueOf(processId).equals(vmDescriptor.id())) {
return true;
}
}
return false;
}
public static boolean isRunning(final Process process) {
try {
process.exitValue();
return false;
}
catch (IllegalThreadStateException ignore) {
return true;
}
}
public static void signalStop(final Process process) throws IOException {
if (isRunning(process)) {
OutputStream processOutputStream = process.getOutputStream();
processOutputStream.write(TERM_TOKEN.concat("\n").getBytes());
processOutputStream.flush();
}
}
public static void waitForStopSignal() {
Scanner in = new Scanner(System.in);
while (!TERM_TOKEN.equals(in.next()));
}
public static int findAndReadPid(final File workingDirectory) {
Assert.isTrue(workingDirectory != null && workingDirectory.isDirectory(), String.format(
"The file system pathname (%1$s) expected to contain a PID file is not a valid directory!",
workingDirectory));
File pidFile = findPidFile(workingDirectory);
if (pidFile == null) {
throw new PidUnavailableException(String.format(
"No PID file was found in working directory (%1$s) or any of it's sub-directories!",
workingDirectory));
}
return readPid(pidFile);
}
protected static File findPidFile(final File workingDirectory) {
Assert.isTrue(workingDirectory != null && workingDirectory.isDirectory(), String.format(
"The file system pathname (%1$s) is not valid directory!", workingDirectory));
for (File file : workingDirectory.listFiles(DirectoryPidFileFilter.INSTANCE)) {
if (file.isDirectory()) {
file = findPidFile(file);
}
if (PidFileFilter.INSTANCE.accept(file)) {
return file;
}
}
return null;
}
public static int readPid(final File pidFile) {
Assert.isTrue(pidFile != null && pidFile.isFile(), String.format(
"The file system pathname (%1$s) is not a valid file!", pidFile));
BufferedReader fileReader = null;
String pidValue = null;
try {
fileReader = new BufferedReader(new FileReader(pidFile));
pidValue = String.valueOf(fileReader.readLine()).trim();
return Integer.parseInt(pidValue);
}
catch (FileNotFoundException e) {
throw new PidUnavailableException(String.format("PID file (%1$s) could not be found!", pidFile), e);
}
catch (IOException e) {
throw new PidUnavailableException(String.format("Unable to read PID from file (%1$s)!", pidFile), e);
}
catch (NumberFormatException e) {
throw new PidUnavailableException(String.format(
"The value (%1$s) from PID file (%2$s) was not a valid numerical PID!", pidValue, pidFile), e);
}
finally {
IOUtils.close(fileReader);
}
}
public static void writePid(final File pidFile, final int pid) throws IOException {
Assert.isTrue(pidFile != null && (pidFile.isFile() || pidFile.createNewFile()), String.format(
"The file system pathname (%1$s) in which the PID will be written is not a valid file!", pidFile));
Assert.isTrue(pid > 0, String.format("The PID value (%1$d) must greater than 0!", pid));
PrintWriter fileWriter = new PrintWriter(new BufferedWriter(new FileWriter(pidFile, false), 16), true);
try {
fileWriter.println(pid);
}
finally {
pidFile.deleteOnExit();
IOUtils.close(fileWriter);
}
}
protected static class DirectoryPidFileFilter extends PidFileFilter {
protected static final DirectoryPidFileFilter INSTANCE = new DirectoryPidFileFilter();
@Override
public boolean accept(final File pathname) {
return (pathname != null && (pathname.isDirectory() || super.accept(pathname)));
}
}
protected static class PidFileFilter implements FileFilter {
protected static final PidFileFilter INSTANCE = new PidFileFilter();
@Override
public boolean accept(final File pathname) {
return (pathname != null && pathname.isFile() && pathname.getName().endsWith(".pid"));
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.test.support;
import java.util.Enumeration;
import java.util.Iterator;
/**
* The CollectionUtils class is a utility class for working with the Java Collections Framework.
*
* @author John Blum
* @see java.util.Collection
* @see java.util.Collections
* @since 1.5.0
*/
@SuppressWarnings("unused")
public abstract class CollectionUtils {
public static <T> Iterable<T> iterable(final Enumeration<T> enumeration) {
return new Iterable<T>() {
@Override public Iterator<T> iterator() {
return org.springframework.util.CollectionUtils.toIterator(enumeration);
}
};
}
public static <T> Iterable<T> iterable(final Iterator<T> iterator) {
return new Iterable<T>() {
@Override public Iterator<T> iterator() {
return iterator;
}
};
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.test.support;
import java.io.Closeable;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* The IOUtils class is an utility class working with IO operations.
*
* @author John Blum
* @since 1.5.0
*/
@SuppressWarnings("unused")
public abstract class IOUtils {
private static final Logger log = Logger.getLogger(IOUtils.class.getName());
public static boolean close(final Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
return true;
}
catch (IOException ignore) {
if (log.isLoggable(Level.FINE)) {
log.fine(String.format("Failed to close Closeable object (%1$s) due to I/O error:%n%2$s",
closeable, ThrowableUtils.toString(ignore)));
}
}
}
return false;
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.test.support;
import java.util.concurrent.TimeUnit;
/**
* The ThreadUtils class is a utility class for working with Java Threads.
*
* @author John Blum
* @see java.lang.Thread
* @since 1.5.0
*/
@SuppressWarnings("unused")
public abstract class ThreadUtils {
public static boolean sleep(final long milliseconds) {
try {
Thread.sleep(milliseconds);
return true;
}
catch (InterruptedException ignore) {
return false;
}
}
public static void timedWait(final long milliseconds) {
timedWait(milliseconds, milliseconds);
}
public static void timedWait(final long milliseconds, final long interval) {
timedWait(milliseconds, interval, new WaitCondition() {
@Override public boolean waiting() {
return true;
}
});
}
public static void timedWait(final long milliseconds, long interval, final WaitCondition waitCondition) {
final long timeout = (System.currentTimeMillis() + milliseconds);
interval = Math.min(interval, milliseconds);
while (waitCondition.waiting() && (System.currentTimeMillis() < timeout)) {
try {
synchronized (waitCondition) {
TimeUnit.MILLISECONDS.timedWait(waitCondition, interval);
}
}
catch (InterruptedException ignore) {
}
}
}
public static interface WaitCondition {
boolean waiting();
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.test.support;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* The ExceptionUtils class is a utility class for working with Throwable, Exception and Error objects.
*
* @author John Blum
* @see java.lang.Error
* @see java.lang.Exception
* @see java.lang.Throwable
* @since 1.5.0
*/
@SuppressWarnings("unused")
public abstract class ThrowableUtils {
public static String toString(Throwable t) {
StringWriter writer = new StringWriter();
t.printStackTrace(new PrintWriter(writer));
return writer.toString();
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2010-2013 the original author or authors.
*
* 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 org.springframework.data.gemfire.test.support;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
/**
* The ZipUtils class is an abstract utility class for working with JAR and ZIP archives.
*
* @author John Blum
* @see java.io.File
* @see java.util.zip.ZipFile
* @since 1.5.0
*/
public abstract class ZipUtils {
public static void unzip(final Resource zipResource, final File directory) throws IOException {
Assert.notNull(zipResource, "The ZIP Resource must not be null!");
Assert.isTrue(directory != null && directory.isDirectory(), String.format(
"The file system pathname (%1$s) is not a valid directory!", directory));
ZipFile zipFile = new ZipFile(zipResource.getFile(), ZipFile.OPEN_READ);
for (ZipEntry entry : CollectionUtils.iterable(zipFile.entries())) {
if (entry.isDirectory()) {
new File(directory, entry.getName()).mkdirs();
}
else {
DataInputStream entryInputStream = new DataInputStream(zipFile.getInputStream(entry));
DataOutputStream entryOutputStream = new DataOutputStream(new FileOutputStream(
new File(directory, entry.getName())));
try {
FileCopyUtils.copy(entryInputStream, entryOutputStream);
}
finally {
IOUtils.close(entryInputStream);
IOUtils.close(entryOutputStream);
}
}
}
}
}