/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.wsf.test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import org.jboss.ws.common.concurrent.CopyJob;
import org.jboss.ws.common.io.TeeOutputStream;
/**
* @author <a href="mailto:ropalka@redhat.com">Richard Opalka</a>
*/
final class AppclientHelper
{
private static final String JBOSS_HOME = System.getProperty("jboss.home");
private static final String FS = System.getProperty("file.separator"); // '/' on unix, '\' on windows
private static final String PS = System.getProperty("path.separator"); // ':' on unix, ';' on windows
private static final int TIMEOUT = Integer.getInteger("appclient.timeout", 120);
private static final String EXT = ":".equals(PS) ? ".sh" : ".bat";
private static final String appclientScript = JBOSS_HOME + FS + "bin" + FS + "appclient" + EXT;
private static final Semaphore s = new Semaphore(1, true); //one appclient only can be running at the same time ATM
private static Map<String, AppclientProcess> appclients = Collections.synchronizedMap(new HashMap<String, AppclientProcess>(2));
private static ExecutorService executors = Executors.newCachedThreadPool(AppclientDaemonFactory.INSTANCE);
private static String appclientOutputDir;
private static class AppclientProcess {
public Process process;
public CopyJob outTask;
public CopyJob errTask;
public OutputStream output;
public OutputStream log;
}
private AppclientHelper()
{
// forbidden instantiation
}
/**
* Triggers appclient deployment and returns the corresponding Process
* Please note the provided output stream (if any) is not automatically closed.
*
* @param archive
* @param appclientOS
* @param appclientArgs
* @return
* @throws Exception
*/
static Process deployAppclient(final String archive, final OutputStream appclientOS, final String... appclientArgs) throws Exception
{
final AppclientProcess ap = newAppclientProcess(archive, appclientOS, appclientArgs);
final String patternToMatch = "Deployed \"" + getAppclientEarName(archive) + "\"";
if (!awaitOutput(ap.output, patternToMatch)) {
throw new RuntimeException("Cannot deploy " + getAppclientFullName(archive) + " to appclient");
}
appclients.put(archive, ap);
return ap.process;
}
static void undeployAppclient(final String archive, boolean awaitShutdown) throws Exception
{
final AppclientProcess ap = appclients.remove(archive);
try
{
if (awaitShutdown)
{
shutdownAppclient(archive, ap.output);
}
}
finally
{
s.release();
//NPE checks to avoid hiding other exceptional conditions that led to premature undeploy..
if (ap != null) {
if (ap.output != null) {
ap.outTask.kill();
}
if (ap.errTask != null) {
ap.errTask.kill();
}
if (ap.process != null) {
ap.process.destroy();
}
if (ap.log != null) {
ap.log.close();
}
}
}
}
private static AppclientProcess newAppclientProcess(final String archive, final OutputStream appclientOS, final String... appclientArgs) throws Exception
{
s.acquire();
try {
final String killFileName = getKillFileName(archive);
final String appclientFullName = getAppclientFullName(archive);
final String appclientShortName = getAppclientShortName(archive);
final AppclientProcess ap = new AppclientProcess();
ap.output = new ByteArrayOutputStream();
final List<String> args = new LinkedList<String>();
args.add(appclientScript);
String appclientConfigName = System.getProperty("APPCLIENT_CONFIG_NAME", "appclient.xml");
String configArg = "--appclient-config=" + appclientConfigName;
args.add(configArg);
args.add(appclientFullName);
if (appclientOS == null)
{
args.add(killFileName);
}
else
{
// propagate appclient args
for (final String appclientArg : appclientArgs)
{
args.add(appclientArg);
}
}
//note on output streams closing: we're not caring about closing any here as it's quite a complex thing due to the TeeOutputStream nesting;
//we're however still safe, given the ap.output is a ByteArrayOutputStream (whose .close() does nothing), ap.log is explicitly closed at
//undeploy and closing appclientOS is a caller responsibility.
ap.log = new FileOutputStream(new File(getAppclientOutputDir(), appclientShortName + ".log-" + System.currentTimeMillis()));
@SuppressWarnings("resource")
final OutputStream logOutputStreams = (appclientOS == null) ? ap.log : new TeeOutputStream(ap.log, appclientOS);
printLogTrailer(logOutputStreams, appclientFullName);
final ProcessBuilder pb = new ProcessBuilder().command(args);
// always propagate IPv6 related properties
final StringBuilder javaOptsValue = new StringBuilder();
String additionalJVMArgs = System.getProperty("additionalJvmArgs");
if (additionalJVMArgs != null) {
javaOptsValue.append(additionalJVMArgs).append(" ");
} else {
javaOptsValue.append("-Djava.net.preferIPv4Stack=").append(System.getProperty("java.net.preferIPv4Stack", "true")).append(" ");
javaOptsValue.append("-Djava.net.preferIPv6Addresses=").append(System.getProperty("java.net.preferIPv6Addresses", "false")).append(" ");
}
javaOptsValue.append("-Djboss.bind.address=").append(undoIPv6Brackets(System.getProperty("jboss.bind.address", "localhost"))).append(" ");
String appclientDebugOpts = System.getProperty("APPCLIENT_DEBUG_OPTS", null);
if (appclientDebugOpts != null && appclientDebugOpts.trim().length() > 0)
javaOptsValue.append(appclientDebugOpts).append(" ");
pb.environment().put("JAVA_OPTS", javaOptsValue.toString());
System.out.println("JAVA_OPTS=\"" + javaOptsValue.toString() + "\"");
System.out.println("Starting " + appclientScript + " " + configArg + " " + appclientFullName + (appclientArgs == null ? "" : " with args " + Arrays.asList(appclientArgs)));
ap.process = pb.start();
// appclient out
ap.outTask = new CopyJob(ap.process.getInputStream(), new TeeOutputStream(ap.output, logOutputStreams));
// appclient err
ap.errTask = new CopyJob(ap.process.getErrorStream(), ap.log);
// unfortunately the following threads are needed because of Windows behavior
executors.submit(ap.outTask);
executors.submit(ap.errTask);
return ap;
} catch (Exception e) {
s.release();
throw e;
}
}
private static void printLogTrailer(OutputStream logOutputStreams, String appclientFullName) {
final PrintWriter pw = new PrintWriter(new OutputStreamWriter(logOutputStreams, StandardCharsets.UTF_8));
pw.write("Starting appclient process: " + appclientFullName + "...\n");
pw.flush();
}
private static String undoIPv6Brackets(final String s)
{
return s.startsWith("[") ? s.substring(1, s.length() - 1) : s;
}
private static void shutdownAppclient(final String archive, final OutputStream os) throws IOException, InterruptedException
{
final File killFile = new File(getKillFileName(archive));
killFile.createNewFile();
try
{
if (!awaitOutput(os, "stopped in")) {
throw new RuntimeException("Cannot undeploy " + getAppclientFullName(archive) + " from appclient");
}
}
finally
{
if (!killFile.delete())
{
killFile.deleteOnExit();
}
}
}
private static boolean awaitOutput(final OutputStream os, final String patternToMatch) throws InterruptedException {
int countOfAttempts = 0;
final int maxCountOfAttempts = TIMEOUT * 2; // max wait time: default 2 minutes
while (!os.toString().contains(patternToMatch))
{
Thread.sleep(500);
if (countOfAttempts++ == maxCountOfAttempts)
{
return false;
}
}
return true;
}
private static String getKillFileName(final String archive)
{
final int sharpIndex = archive.indexOf('#');
return JBOSS_HOME + FS + "bin" + FS + archive.substring(sharpIndex + 1) + ".kill";
}
private static String getAppclientOutputDir()
{
if (appclientOutputDir == null)
{
appclientOutputDir = System.getProperty("appclient.output.dir");
if (appclientOutputDir == null)
{
throw new IllegalStateException("System property appclient.output.dir not configured");
}
final File appclientOutputDirectory = new File(appclientOutputDir);
if (!appclientOutputDirectory.exists())
{
if (!appclientOutputDirectory.mkdirs())
{
throw new IllegalStateException("Unable to create directory " + appclientOutputDir);
}
}
}
return appclientOutputDir;
}
private static String getAppclientFullName(final String archive)
{
final int sharpIndex = archive.indexOf('#');
final String earName = archive.substring(0, sharpIndex);
return JBossWSTestHelper.getArchiveFile(earName).getParent() + FS + archive;
}
private static String getAppclientShortName(final String archive)
{
final int sharpIndex = archive.indexOf('#');
return archive.substring(sharpIndex + 1);
}
private static String getAppclientEarName(final String archive)
{
final int sharpIndex = archive.indexOf('#');
return archive.substring(0, sharpIndex);
}
// [JBPAPP-10027] appclient threads are always daemons (to don't block JVM shutdown)
private static class AppclientDaemonFactory implements ThreadFactory {
static final AppclientDaemonFactory INSTANCE = new AppclientDaemonFactory();
final ThreadGroup group;
final AtomicInteger threadNumber = new AtomicInteger(1);
final String namePrefix;
AppclientDaemonFactory() {
group = Thread.currentThread().getThreadGroup();
namePrefix = "appclient-output-processing-daemon-";
}
public Thread newThread(final Runnable r) {
final Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement());
t.setDaemon(true);
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
}