This manager will be used to a higher level by ProcessManager.
Change-Id: Ifaf2ec386885bdf4ae294a06ef08a18cd6d1e630
--- /dev/null
+package org.tizen.dynamicanalyzer.cli.manager;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import org.tizen.dynamicanalyzer.cli.tracing.TracingArguments;
+
+/**
+ * Class represents state of tracing process.
+ * State doesn't refresh automatically.
+ */
+public class TracingProcessContext implements Cloneable, Serializable {
+ /**
+ * Autogenerated class version number.
+ */
+ private static final long serialVersionUID = -5458874901547893614L;
+
+ /**
+ * Tracing arguments with which the tracing process started.
+ */
+ private TracingArguments args;
+
+ /**
+ * Flag that shows whether process is running or finished.
+ */
+ private boolean finished;
+
+ /**
+ * Code with which tracing process exited (if <code>finished == true</code>).
+ */
+ private int errCode;
+
+ /**
+ * Denotes time when tracing process started.
+ */
+ private Date startTime;
+
+ /**
+ * Denotes time when tracing process was finished (if <code>finished == true</code>).
+ */
+ private Date finishTime;
+
+ /**
+ * Returns tracing arguments.
+ */
+ public TracingArguments getArgs() {
+ return args;
+ }
+
+ /**
+ * Returns flag shows whether process is running or finished.
+ */
+ public boolean isFinished() {
+ return finished;
+ }
+
+ /**
+ * Returns process error code (exit code).
+ * This method has meaning only if process is already finished.
+ */
+ public int getErrCode() {
+ return errCode;
+ }
+
+ /**
+ * Returns process start time.
+ */
+ public Date getStartTime() {
+ return (Date) startTime.clone();
+ }
+
+ /**
+ * Returns process finish time.
+ * This method has meaning only if process is already finished.
+ */
+ public Date getFinishTime() {
+ return (Date) finishTime.clone();
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param args arguments with which tracing process was started
+ */
+ public TracingProcessContext(TracingArguments args) {
+ this.args = args;
+
+ finished = false;
+ startTime = new Date();
+ }
+
+ /**
+ * Sets undefined fields of context.
+ * After calling this method {@link #isFinished()} always return true
+ * {@link #getFinishTime()}, {@link #getErrCode()} return meaningful values.
+ *
+ * @param errCode code with which process has finished
+ */
+ public void finishContext(int errCode) {
+ if (finished)
+ return;
+
+ this.errCode = errCode;
+
+ finished = true;
+ finishTime = new Date();
+ }
+
+ /**
+ * Performs deep copy of this context.
+ */
+ @Override
+ public TracingProcessContext clone() {
+ TracingProcessContext copy = null;
+ try {
+ copy = (TracingProcessContext) super.clone();
+ } catch (CloneNotSupportedException e) {
+ // never should go here
+ throw new AssertionError(e);
+ }
+
+ // deep copy
+ copy.args = args.clone();
+ copy.startTime = (Date) startTime.clone();
+ if (finishTime != null)
+ copy.finishTime = (Date) finishTime.clone();
+
+ return copy;
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.tizen.dynamicanalyzer.cli.manager;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ProcessBuilder.Redirect;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.tizen.dynamicanalyzer.cli.tracing.TracingArguments;
+import org.tizen.dynamicanalyzer.cli.tracing.TracingArgumentsParser;
+import org.tizen.dynamicanalyzer.cli.tracing.TracingProcess;
+
+/**
+ * Class supposed to manage single {@link TracingProcess} instance.
+ * State of underlying tracing process is asynchronously monitored.
+ */
+public class TracingProcessManager {
+ private final static String TRACING_PROCESS_CANONICAL_NAME = TracingProcess.class.getCanonicalName();
+
+ private final static String TRACING_PROCESS_LIBRARY_PATH = getTracingProcessLibraryPath();
+
+ private static String getTracingProcessLibraryPath() {
+ try {
+ return new File(TracingProcess.class.getProtectionDomain().getCodeSource().getLocation().getPath()).getAbsolutePath();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Execute new tracing process with specified arguments and wrap it with {@link TracingProcessManager}.
+ *
+ * @param args tracing arguments
+ * @return {@link TracingProcessManager} instance that manages corresponding process.
+ * @throws IOException in error occurred while executing process
+ */
+ public static TracingProcessManager createTracingProcess(TracingArguments args) throws IOException {
+ // compose command line
+ String currentClasspath = System.getProperty("java.class.path");
+
+ List<String> commandLine = new ArrayList<String>(Arrays.asList(
+ "java",
+
+ // set classpath
+ "-classpath",
+ TRACING_PROCESS_LIBRARY_PATH + ":" + currentClasspath,
+
+ // set main executable class
+ TRACING_PROCESS_CANONICAL_NAME
+ ));
+
+ // add tracing arguments
+ commandLine.addAll(Arrays.asList(TracingArgumentsParser.toStringArray(args)));
+
+ // redirect I/O
+ ProcessBuilder pBuilder = new ProcessBuilder(commandLine)
+ .redirectInput(Redirect.PIPE)
+ .redirectOutput(Redirect.INHERIT)
+ .redirectError(Redirect.INHERIT);
+
+ // actually start separate tracing process
+ Process process = pBuilder.start();
+
+ return new TracingProcessManager(args, process);
+ }
+
+ /**
+ * State of managed tracing process.
+ */
+ private TracingProcessContext ctx;
+
+ /**
+ * Underlying process instance corresponding to the tracing process.
+ */
+ private Process tracingProcess;
+
+ /**
+ * Thread that watches underlying process state.
+ */
+ private volatile Thread monitoringThread;
+
+ /**
+ * Launch asynchronous monitoring of tracing process state.
+ */
+ private void startMonitoring() {
+ monitoringThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ waitForCompletion();
+ } catch (InterruptedException e) {
+ // nothing to do
+ }
+ }
+ }, "wait-completion-thread-" + ctx.getArgs().getDevice());
+
+ monitoringThread.setDaemon(true);
+ monitoringThread.start();
+ }
+
+ /**
+ * Private constructor.
+ * Instances should be created via {@link #createTracingProcess(TracingArguments)}.
+ *
+ * @param args arguments with which tracing process started
+ * @param process process instance corresponding to the tracing process
+ */
+ private TracingProcessManager(TracingArguments args, Process process) {
+ ctx = new TracingProcessContext(args);
+ tracingProcess = process;
+
+ startMonitoring();
+ }
+
+ /**
+ * This method blocks caller thread until tracing process will be finished.
+ *
+ * @throws InterruptedException if waiting was interrupted
+ */
+ public void waitForCompletion() throws InterruptedException {
+ int errCode;
+
+ errCode = tracingProcess.waitFor();
+
+ synchronized (this) {
+ ctx.finishContext(errCode);
+ }
+ }
+
+ /**
+ * Stop tracing process.
+ * This method blocks caller thread until traced process will be finished.
+ *
+ * @throws InterruptedException if wait for finish is interrupted
+ * @throws IOException if communication error occurred
+ */
+ public synchronized void stopTracing() throws InterruptedException, IOException {
+ if (ctx.isFinished())
+ return;
+
+ // try to send stop signal to the tracing process
+ // by closing it's input stream
+ tracingProcess.getOutputStream().close();
+
+ waitForCompletion();
+ }
+
+ /**
+ * Stop tracing process.
+ * This method blocks caller thread at most specified amount of time.
+ *
+ * @param timeoutMs time to wait until interrupt
+ * @return <code>true</code> if process was finished finally, <code>false</code> otherwise
+ * @throws InterruptedException if wait interrupted before timeout fired
+ */
+ public boolean stopTracing(long timeoutMs) throws InterruptedException {
+ synchronized (this) {
+ if (ctx.isFinished())
+ return true;
+ }
+
+ Thread workingThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ stopTracing();
+ } catch (IOException | InterruptedException e) {
+ // nothing to do
+ }
+ }
+ }, "stop-tracing-thread-" + ctx.getArgs().getDevice());
+
+ workingThread.start();
+ workingThread.join(timeoutMs);
+ if (workingThread.isAlive()) {
+ workingThread.interrupt();
+ }
+
+ synchronized (this) {
+ return ctx.isFinished();
+ }
+ }
+
+ /**
+ * Forcibly terminate underlying tracing process.
+ */
+ public synchronized void forceStopTracing() {
+ if (ctx.isFinished())
+ return;
+
+ // forcibly terminate tracing process
+ tracingProcess.destroy();
+
+ try {
+ waitForCompletion();
+ } catch (InterruptedException e) {
+ // shouldn't ever go here
+ throw new AssertionError("Something goes wrong while destroying tracing process.");
+ }
+ }
+
+ /**
+ * Return current state of tracing process.
+ *
+ * @return tracing process state
+ */
+ public synchronized TracingProcessContext getContext() {
+ return ctx.clone();
+ }
+
+ /**
+ * Return whether tracing process is running or not.
+ * This state automatically updated.
+ *
+ * @return <code>true</code> if tracing process finished <br>
+ * <code>false</code> otherwise
+ */
+ public synchronized boolean isFinished() {
+ return ctx.isFinished();
+ }
+
+}
--- /dev/null
+package org.tizen.dynamicanalyzer.cli.manager;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.OutputStream;
+import java.lang.reflect.Constructor;
+import java.util.Date;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.powermock.reflect.Whitebox;
+import org.tizen.dynamicanalyzer.cli.tracing.TracingArguments;
+import org.tizen.dynamicanalyzer.setting.Template;
+
+/**
+ * Test for {@link TracingProcessManager} instances.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({TracingProcessManager.class})
+public class TracingProcessManagerTest {
+ TracingArguments args;
+
+ @Mock
+ Process process;
+
+ @Mock
+ OutputStream oStream;
+
+ /**
+ * Base time unit used in some test for sleep and timeout.
+ */
+ static final int TIMEOUT_MS = 500;
+
+ /**
+ * Time measurement precision.
+ */
+ static final int TIME_EPS_MS = 50;
+
+ /**
+ * Some code to return in tests.
+ */
+ static final int CODE_TO_RETURN = 100500;
+
+ /**
+ * Class under test
+ */
+ TracingProcessManager manager;
+
+ /**
+ * Private constructor of {@link TracingProcessManager}.
+ */
+ Constructor<TracingProcessManager> managerConstructor;
+
+ /**
+ * Utility method compares specified time with current system time.
+ */
+ private void assertCurrentTime(long time_ms) {
+ assertTrue(Math.abs(time_ms - new Date().getTime()) < TIME_EPS_MS);
+ }
+
+ @Before
+ public void setUp() {
+ when(process.getOutputStream()).thenReturn(oStream);
+
+ // init tracing arguments
+ args = new TracingArguments();
+ args.setDevice("DEV");
+ args.setApplication("APP");
+ args.setDuration(0);
+ args.setTemplate(Template.TEMPLATE_BOTTLENECK);
+
+ managerConstructor = Whitebox.getConstructor(TracingProcessManager.class, TracingArguments.class, Process.class);
+ }
+
+ /**
+ * Test manager ability to wait for tracing process completion.
+ */
+ @Test(timeout=2*TIMEOUT_MS)
+ public void waitForCompletion_block_no_except() throws Exception {
+ TracingProcessContext ctx = null;
+
+ when(process.waitFor()).thenAnswer(new Answer<Integer>() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ Thread.sleep(TIMEOUT_MS);
+ return CODE_TO_RETURN;
+ }
+ });
+
+ // create class under test
+ manager = managerConstructor.newInstance(args, process);
+ ctx = manager.getContext();
+
+ assertFalse(manager.isFinished());
+ assertFalse(ctx.isFinished());
+ assertCurrentTime(ctx.getStartTime().getTime());
+
+ manager.waitForCompletion();
+
+ ctx = manager.getContext();
+ assertTrue(ctx.isFinished());
+ assertCurrentTime(ctx.getFinishTime().getTime());
+ assertTrue(manager.isFinished());
+ assertEquals(CODE_TO_RETURN, manager.getContext().getErrCode());
+ }
+
+ /**
+ * Test that manager correctly sends asynchronous stop signal to tracing process.
+ */
+ @Test
+ public void stopTracing_no_except() throws Exception {
+ // create class under test
+ manager = managerConstructor.newInstance(args, process);
+ manager.stopTracing();
+
+ verify(oStream).close();
+ }
+
+ /**
+ * Test that manager correctly handles successful tracing process
+ * completion and synchronously returns error code.
+ */
+ @Test(timeout=3*TIMEOUT_MS)
+ public void stopTracing_timeout_dont_trigger_no_except() throws Exception {
+ boolean result = false;
+
+ when(process.waitFor()).thenAnswer(new Answer<Integer>() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ Thread.sleep(TIMEOUT_MS);
+ return CODE_TO_RETURN;
+ }
+ });
+
+ // create class under test
+ manager = managerConstructor.newInstance(args, process);
+
+ assertFalse(manager.isFinished());
+ assertCurrentTime(manager.getContext().getStartTime().getTime());
+
+ result = manager.stopTracing(2*TIMEOUT_MS);
+
+ TracingProcessContext ctx = manager.getContext();
+ assertTrue(result);
+ assertTrue(manager.isFinished());
+ assertCurrentTime(ctx.getFinishTime().getTime());
+ assertEquals(ctx.getErrCode(), CODE_TO_RETURN);
+ }
+
+ /**
+ * Test that manager correctly handles case when tracing process
+ * hangs for a long time and tracing process was terminated.
+ */
+ @Test(timeout=3*TIMEOUT_MS)
+ public void stopTracing_timeout_trigger_no_except() throws Exception {
+ boolean result = false;
+
+ when(process.waitFor()).thenAnswer(new Answer<Integer>() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws InterruptedException {
+ try {
+ Thread.sleep(2*TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ throw e;
+ }
+ return null;
+ }
+ });
+
+ // create class under test
+ manager = managerConstructor.newInstance(args, process);
+
+ assertFalse(manager.isFinished());
+ assertCurrentTime(manager.getContext().getStartTime().getTime());
+
+ result = manager.stopTracing(TIMEOUT_MS);
+
+ assertFalse(result);
+ assertFalse(manager.isFinished());
+ }
+
+ /**
+ * Guarded variable used in {@link #forceStopTracing_no_except()} test.
+ */
+ volatile boolean destroy_called;
+ Object destroy_lock = new Object();
+
+
+ /**
+ * Check ability of manager to forcibly stop hanging tracing process.
+ */
+ @Test(timeout=TIMEOUT_MS)
+ public void forceStopTracing_no_except() throws Exception {
+ destroy_called = false;
+
+ when(process.waitFor()).thenAnswer(new Answer<Integer>() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws InterruptedException {
+ synchronized (destroy_lock) {
+ while (!destroy_called) {
+ destroy_lock.wait();
+ }
+ }
+ return CODE_TO_RETURN;
+ }
+ });
+
+ doAnswer(new Answer<Object>() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) {
+ synchronized (destroy_lock) {
+ destroy_called = true;
+ destroy_lock.notifyAll();
+ }
+ return null;
+ }
+ }).when(process).destroy();
+
+ // create class under test
+ manager = managerConstructor.newInstance(args, process);
+
+ assertFalse(manager.isFinished());
+ assertCurrentTime(manager.getContext().getStartTime().getTime());
+
+ manager.forceStopTracing();
+
+ verify(process).destroy();
+ verify(process, Mockito.atLeastOnce()).waitFor();
+
+ TracingProcessContext ctx = manager.getContext();
+ assertTrue(manager.isFinished());
+ assertEquals(ctx.getErrCode(), CODE_TO_RETURN);
+ assertCurrentTime(ctx.getFinishTime().getTime());
+ }
+}