SRADA-215: introduced manager for single tracing process
authorVladislav Eliseev <v.eliseev@samsung.com>
Fri, 22 Apr 2016 13:54:12 +0000 (16:54 +0300)
committerVladislav Eliseev <v.eliseev@samsung.com>
Fri, 22 Apr 2016 13:57:39 +0000 (16:57 +0300)
This manager will be used to a higher level by ProcessManager.

Change-Id: Ifaf2ec386885bdf4ae294a06ef08a18cd6d1e630

org.tizen.dynamicanalyzer.cli/src/org/tizen/dynamicanalyzer/cli/manager/TracingProcessContext.java [new file with mode: 0644]
org.tizen.dynamicanalyzer.cli/src/org/tizen/dynamicanalyzer/cli/manager/TracingProcessManager.java [new file with mode: 0644]
org.tizen.dynamicanalyzer.cli/test/src/org/tizen/dynamicanalyzer/cli/manager/TracingProcessManagerTest.java [new file with mode: 0644]

diff --git a/org.tizen.dynamicanalyzer.cli/src/org/tizen/dynamicanalyzer/cli/manager/TracingProcessContext.java b/org.tizen.dynamicanalyzer.cli/src/org/tizen/dynamicanalyzer/cli/manager/TracingProcessContext.java
new file mode 100644 (file)
index 0000000..a1987fa
--- /dev/null
@@ -0,0 +1,130 @@
+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
diff --git a/org.tizen.dynamicanalyzer.cli/src/org/tizen/dynamicanalyzer/cli/manager/TracingProcessManager.java b/org.tizen.dynamicanalyzer.cli/src/org/tizen/dynamicanalyzer/cli/manager/TracingProcessManager.java
new file mode 100644 (file)
index 0000000..eab617e
--- /dev/null
@@ -0,0 +1,223 @@
+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();
+       }
+
+}
diff --git a/org.tizen.dynamicanalyzer.cli/test/src/org/tizen/dynamicanalyzer/cli/manager/TracingProcessManagerTest.java b/org.tizen.dynamicanalyzer.cli/test/src/org/tizen/dynamicanalyzer/cli/manager/TracingProcessManagerTest.java
new file mode 100644 (file)
index 0000000..3fe8b75
--- /dev/null
@@ -0,0 +1,247 @@
+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());
+       }
+}