/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library 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 library 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. */ package com.liferay.portal.kernel.process; import com.liferay.portal.kernel.concurrent.ThreadPoolExecutor; import com.liferay.portal.kernel.test.CaptureHandler; import com.liferay.portal.kernel.test.JDKLoggerTestUtil; import com.liferay.portal.kernel.test.SyncThrowableThread; import com.liferay.portal.kernel.test.rule.CodeCoverageAssertor; import com.liferay.portal.kernel.util.ObjectValuePair; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.LogRecord; import org.junit.After; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; /** * @author Shuyang Zhou */ public class ProcessUtilTest { @ClassRule public static final CodeCoverageAssertor codeCoverageAssertor = CodeCoverageAssertor.INSTANCE; @After public void tearDown() throws Exception { ExecutorService executorService = _getExecutorService(); if (executorService != null) { executorService.shutdownNow(); executorService.awaitTermination(10, TimeUnit.SECONDS); _nullOutThreadPoolExecutor(); } } @Test public void testConcurrentCreateExecutorService() throws Exception { final AtomicReference<ExecutorService> atomicReference = new AtomicReference<>(); SyncThrowableThread<Void> syncThrowableThread = new SyncThrowableThread<>( new Callable<Void>() { @Override public Void call() throws Exception { ExecutorService executorService = _invokeGetThreadPoolExecutor(); atomicReference.set(executorService); return null; } }); ExecutorService executorService = null; synchronized (ProcessUtil.class) { syncThrowableThread.start(); while (syncThrowableThread.getState() != Thread.State.BLOCKED); executorService = _invokeGetThreadPoolExecutor(); } syncThrowableThread.sync(); Assert.assertSame(executorService, atomicReference.get()); _invokeGetThreadPoolExecutor(); } @Test public void testDestroy() throws Exception { // Clean destroy ProcessUtil processUtil = new ProcessUtil(); processUtil.destroy(); Assert.assertNull(_getExecutorService()); // Idle destroy ExecutorService executorService = _invokeGetThreadPoolExecutor(); Assert.assertNotNull(executorService); Assert.assertNotNull(_getExecutorService()); processUtil.destroy(); Assert.assertNull(_getExecutorService()); // Busy destroy executorService = _invokeGetThreadPoolExecutor(); Assert.assertNotNull(executorService); Assert.assertNotNull(_getExecutorService()); DummyJob dummyJob = new DummyJob(); Future<Void> future = executorService.submit(dummyJob); dummyJob.waitUntilStarted(); processUtil.destroy(); try { future.get(); Assert.fail(); } catch (ExecutionException ee) { Throwable throwable = ee.getCause(); Assert.assertTrue(throwable instanceof InterruptedException); } Assert.assertNull(_getExecutorService()); // Concurrent destroy _invokeGetThreadPoolExecutor(); final ProcessUtil referenceProcessUtil = processUtil; Thread thread = new Thread() { @Override public void run() { referenceProcessUtil.destroy(); } }; synchronized (ProcessUtil.class) { thread.start(); while (thread.getState() != Thread.State.BLOCKED); processUtil.destroy(); } thread.join(); _invokeGetThreadPoolExecutor(); processUtil.destroy(); // Destroy after destroyed processUtil.destroy(); Assert.assertNull(_getExecutorService()); } @Test public void testEcho() throws Exception { // Logging try (CaptureHandler captureHandler = JDKLoggerTestUtil.configureJDKLogger( LoggingOutputProcessor.class.getName(), Level.INFO)) { List<LogRecord> logRecords = captureHandler.getLogRecords(); Future<ObjectValuePair<Void, Void>> loggingFuture = ProcessUtil.execute( ProcessUtil.LOGGING_OUTPUT_PROCESSOR, _buildArguments(Echo.class, "2")); loggingFuture.get(); loggingFuture.cancel(true); List<String> messageRecords = new ArrayList<>(); for (LogRecord logRecord : logRecords) { messageRecords.add(logRecord.getMessage()); } Assert.assertTrue( messageRecords.contains(Echo.buildMessage(false, 0))); Assert.assertTrue( messageRecords.contains(Echo.buildMessage(false, 1))); Assert.assertTrue( messageRecords.contains(Echo.buildMessage(true, 0))); Assert.assertTrue( messageRecords.contains(Echo.buildMessage(true, 1))); } // Collector Future<ObjectValuePair<byte[], byte[]>> collectorFuture = ProcessUtil.execute( ProcessUtil.COLLECTOR_OUTPUT_PROCESSOR, _buildArguments(Echo.class, "2")); ObjectValuePair<byte[], byte[]> objectValuePair = collectorFuture.get(); collectorFuture.cancel(true); Assert.assertEquals( Echo.buildMessage(true, 0) + "\n" + Echo.buildMessage(true, 1) + "\n", new String(objectValuePair.getKey())); Assert.assertEquals( Echo.buildMessage(false, 0) + "\n" + Echo.buildMessage(false, 1) + "\n", new String(objectValuePair.getValue())); } @Test public void testErrorExit() throws Exception { Future<?> future = ProcessUtil.execute( ProcessUtil.CONSUMER_OUTPUT_PROCESSOR, _buildArguments(ErrorExit.class)); try { future.get(); Assert.fail(); } catch (ExecutionException ee) { Throwable throwable = ee.getCause(); Assert.assertSame( TerminationProcessException.class, throwable.getClass()); Assert.assertEquals( "Subprocess terminated with exit code " + ErrorExit.EXIT_CODE, throwable.getMessage()); TerminationProcessException terminationProcessException = (TerminationProcessException)throwable; Assert.assertEquals( ErrorExit.EXIT_CODE, terminationProcessException.getExitCode()); } } @Test public void testErrorOutputProcessor() throws Exception { String[] arguments = _buildArguments(Echo.class, "1"); Future<?> future = ProcessUtil.execute( new ErrorStderrOutputProcessor(), arguments); try { future.get(); Assert.fail(); } catch (ExecutionException ee) { Throwable throwable = ee.getCause(); Assert.assertEquals(ProcessException.class, throwable.getClass()); Assert.assertEquals( ErrorStderrOutputProcessor.class.getName(), throwable.getMessage()); } future = ProcessUtil.execute( new ErrorStdoutOutputProcessor(), arguments); try { future.get(); Assert.fail(); } catch (ExecutionException ee) { Throwable throwable = ee.getCause(); Assert.assertEquals(ProcessException.class, throwable.getClass()); Assert.assertEquals( ErrorStdoutOutputProcessor.class.getName(), throwable.getMessage()); } } @Test public void testExecuteAfterShutdown() throws Exception { ExecutorService executorService = _invokeGetThreadPoolExecutor(); executorService.shutdown(); try { ProcessUtil.execute( ProcessUtil.LOGGING_OUTPUT_PROCESSOR, _buildArguments(Echo.class, "2")); Assert.fail(); } catch (ProcessException pe) { Throwable throwable = pe.getCause(); Assert.assertEquals( RejectedExecutionException.class, throwable.getClass()); } } @Test public void testFuture() throws Exception { // Time out on standard error processing String[] arguments = _buildArguments(Pause.class); Future<?> future = ProcessUtil.execute( ProcessUtil.CONSUMER_OUTPUT_PROCESSOR, arguments); Assert.assertFalse(future.isCancelled()); Assert.assertFalse(future.isDone()); try { future.get(1, TimeUnit.SECONDS); Assert.fail(); } catch (TimeoutException te) { } future.cancel(true); // Cancel twice to satisfy code coverage future.cancel(true); // Time out on standard out processing future = ProcessUtil.execute( new ConsumerOutputProcessor() { @Override public Void processStdErr(InputStream stdOutInputStream) { return null; } }, arguments); Assert.assertFalse(future.isCancelled()); Assert.assertFalse(future.isDone()); try { future.get(1, TimeUnit.SECONDS); Assert.fail(); } catch (TimeoutException te) { } future.cancel(true); // Success time out get future = ProcessUtil.execute( ProcessUtil.CONSUMER_OUTPUT_PROCESSOR, _buildArguments(Echo.class, "0")); future.get(1, TimeUnit.MINUTES); } @Test public void testInterruptPause() throws Exception { final CountDownLatch countDownLatch = new CountDownLatch(1); final Thread mainThread = Thread.currentThread(); SyncThrowableThread<Void> syncThrowableThread = new SyncThrowableThread<>( new Callable<Void>() { @Override public Void call() throws Exception { countDownLatch.await(); while (mainThread.getState() != Thread.State.WAITING); ExecutorService executorService = _getExecutorService(); executorService.shutdownNow(); return null; } }); syncThrowableThread.start(); final Future<?> future = ProcessUtil.execute( new OutputProcessor<Void, Void>() { @Override public Void processStdErr(InputStream stdErrInputStream) { return null; } @Override public Void processStdOut(InputStream stdOutInputStream) { return null; } }, _buildArguments(Pause.class)); try { countDownLatch.countDown(); future.get(); Assert.fail(); } catch (ExecutionException ee) { Throwable throwable = ee.getCause(); Assert.assertEquals(ProcessException.class, throwable.getClass()); Assert.assertEquals( "Forcibly killed subprocess on interruption", throwable.getMessage()); } finally { syncThrowableThread.sync(); } } @Test public void testWrongArguments() throws ProcessException { try { ProcessUtil.execute(null, (List<String>)null); Assert.fail(); } catch (NullPointerException npe) { Assert.assertEquals("Output processor is null", npe.getMessage()); } try { ProcessUtil.execute( ProcessUtil.CONSUMER_OUTPUT_PROCESSOR, (List<String>)null); Assert.fail(); } catch (NullPointerException npe) { Assert.assertEquals("Arguments is null", npe.getMessage()); } try { ProcessUtil.execute( ProcessUtil.CONSUMER_OUTPUT_PROCESSOR, "commandNotExist"); Assert.fail(); } catch (ProcessException pe) { Throwable throwable = pe.getCause(); Assert.assertEquals(IOException.class, throwable.getClass()); } } private static String[] _buildArguments( Class<?> clazz, String... arguments) { List<String> argumentsList = new ArrayList<>(); argumentsList.add("java"); argumentsList.add("-cp"); argumentsList.add(_CLASS_PATH); argumentsList.add(clazz.getName()); Collections.addAll(argumentsList, arguments); return argumentsList.toArray(new String[argumentsList.size()]); } private static ThreadPoolExecutor _getExecutorService() throws Exception { Field field = ProcessUtil.class.getDeclaredField("_threadPoolExecutor"); field.setAccessible(true); return (ThreadPoolExecutor)field.get(null); } private static ThreadPoolExecutor _invokeGetThreadPoolExecutor() throws Exception { Method method = ProcessUtil.class.getDeclaredMethod( "_getThreadPoolExecutor"); method.setAccessible(true); return (ThreadPoolExecutor)method.invoke(method); } private static void _nullOutThreadPoolExecutor() throws Exception { Field field = ProcessUtil.class.getDeclaredField("_threadPoolExecutor"); field.setAccessible(true); field.set(null, null); } private static final String _CLASS_PATH; private static class DummyJob implements Callable<Void> { public DummyJob() { _countDownLatch = new CountDownLatch(1); } @Override public Void call() throws Exception { _countDownLatch.countDown(); Thread.sleep(Long.MAX_VALUE); return null; } public void waitUntilStarted() throws InterruptedException { _countDownLatch.await(); } private final CountDownLatch _countDownLatch; } private static class Echo { public static String buildMessage(boolean stdOut, int number) { if (stdOut) { return "{stdOut}" + Echo.class.getName() + number; } return "{stdErr}" + Echo.class.getName() + number; } @SuppressWarnings("unused") public static void main(String[] arguments) { int times = Integer.parseInt(arguments[0]); for (int i = 0; i < times; i++) { System.err.println(buildMessage(false, i)); System.out.println(buildMessage(true, i)); } } } private static class ErrorExit { public static final int EXIT_CODE = 10; @SuppressWarnings("unused") public static void main(String[] arguments) { System.exit(EXIT_CODE); } } private static class ErrorStderrOutputProcessor implements OutputProcessor<Void, Void> { @Override public Void processStdErr(InputStream stdErrInputStream) throws ProcessException { throw new ProcessException( ErrorStderrOutputProcessor.class.getName()); } @Override public Void processStdOut(InputStream stdOutInputStream) { return null; } } private static class ErrorStdoutOutputProcessor implements OutputProcessor<Void, Void> { @Override public Void processStdErr(InputStream stdErrInputStream) { return null; } @Override public Void processStdOut(InputStream stdOutInputStream) throws ProcessException { throw new ProcessException( ErrorStdoutOutputProcessor.class.getName()); } } private static class Pause { @SuppressWarnings("unused") public static void main(String[] arguments) throws Exception { Thread.sleep(Long.MAX_VALUE); } } static { Class<?> clazz = Echo.class; ClassLoader classLoader = clazz.getClassLoader(); String className = clazz.getName(); String name = className.replace('.', '/') + ".class"; URL url = classLoader.getResource(name); String path = url.getPath(); int index = path.lastIndexOf(name); _CLASS_PATH = path.substring(0, index); } }