/******************************************************************************* * Copyright (c) 2016 Ericsson and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package org.eclipse.cdt.tests.dsf.gdb.tests.nonstop; import static org.junit.Assert.assertEquals; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.eclipse.cdt.debug.core.ICDTLaunchConfigurationConstants; import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor; import org.eclipse.cdt.dsf.concurrent.ImmediateRequestMonitor; import org.eclipse.cdt.dsf.concurrent.Query; import org.eclipse.cdt.dsf.datamodel.DMContexts; import org.eclipse.cdt.dsf.datamodel.IDMContext; import org.eclipse.cdt.dsf.datamodel.IDMEvent; import org.eclipse.cdt.dsf.debug.service.IMultiRunControl; import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerDMContext; import org.eclipse.cdt.dsf.debug.service.IStack.IFrameDMContext; import org.eclipse.cdt.dsf.gdb.IGDBLaunchConfigurationConstants; import org.eclipse.cdt.dsf.gdb.internal.service.IGDBFocusSynchronizer; import org.eclipse.cdt.dsf.gdb.internal.service.IGDBFocusSynchronizer.IGDBFocusChangedEvent; import org.eclipse.cdt.dsf.gdb.service.command.IGDBControl; import org.eclipse.cdt.dsf.mi.service.IMIExecutionDMContext; import org.eclipse.cdt.dsf.mi.service.IMIProcesses; import org.eclipse.cdt.dsf.mi.service.command.commands.CLICommand; import org.eclipse.cdt.dsf.mi.service.command.events.MIStoppedEvent; import org.eclipse.cdt.dsf.mi.service.command.output.CLIThreadInfo; import org.eclipse.cdt.dsf.mi.service.command.output.MIConsoleStreamOutput; import org.eclipse.cdt.dsf.mi.service.command.output.MIConst; import org.eclipse.cdt.dsf.mi.service.command.output.MIInfo; import org.eclipse.cdt.dsf.mi.service.command.output.MINotifyAsyncOutput; import org.eclipse.cdt.dsf.mi.service.command.output.MIOOBRecord; import org.eclipse.cdt.dsf.mi.service.command.output.MIOutput; import org.eclipse.cdt.dsf.mi.service.command.output.MIResult; import org.eclipse.cdt.dsf.mi.service.command.output.MITuple; import org.eclipse.cdt.dsf.mi.service.command.output.MIValue; import org.eclipse.cdt.dsf.service.DsfServiceEventHandler; import org.eclipse.cdt.dsf.service.DsfServicesTracker; import org.eclipse.cdt.dsf.service.DsfSession; import org.eclipse.cdt.tests.dsf.gdb.framework.BaseParametrizedTestCase; import org.eclipse.cdt.tests.dsf.gdb.framework.ServiceEventWaitor; import org.eclipse.cdt.tests.dsf.gdb.framework.SyncUtil; import org.eclipse.cdt.tests.dsf.gdb.launching.TestsPlugin; import org.eclipse.cdt.tests.dsf.gdb.tests.ITestConstants; import org.junit.Assert; import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; public class ThreadStackFrameSyncTest extends BaseParametrizedTestCase { final static private int DEFAULT_TIMEOUT = 1000; private DsfServicesTracker fServicesTracker; private IMultiRunControl fMultiRunControl; private IGDBControl fCommandControl; private IGDBFocusSynchronizer fGdbSync; private DsfSession fSession; private List<IDMEvent<? extends IDMContext>> fEventsReceived = new ArrayList<IDMEvent<? extends IDMContext>>(); // Breakpoint tags in MultiThread.cc public static final String[] LINE_TAGS = new String[] { "LINE_MAIN_BEFORE_THREAD_START", // Just before StartThread "LINE_MAIN_AFTER_THREAD_START", // Just after StartThread "LINE_MAIN_ALL_THREADS_STARTED", // Where all threads are guaranteed to be started. }; /* * Name of the executable */ private static final String EXEC_NAME = "MultiThread.exe"; private static final String SOURCE_NAME = "MultiThread.cc"; @BeforeClass public static void beforeClass() { Assume.assumeTrue(supportsNonStop()); } @Override public void doBeforeTest() throws Exception { assumeGdbVersionAtLeast(ITestConstants.SUFFIX_GDB_7_12); super.doBeforeTest(); resolveLineTagLocations(SOURCE_NAME, LINE_TAGS); fSession = getGDBLaunch().getSession(); Assert.assertNotNull(fSession); Runnable runnable = new Runnable() { @Override public void run() { fServicesTracker = new DsfServicesTracker(TestsPlugin.getBundleContext(), fSession.getId()); Assert.assertTrue(fServicesTracker != null); fCommandControl = fServicesTracker.getService(IGDBControl.class); Assert.assertTrue(fCommandControl != null); fMultiRunControl = fServicesTracker.getService(IMultiRunControl.class); Assert.assertTrue(fMultiRunControl != null); fGdbSync = fServicesTracker.getService(IGDBFocusSynchronizer.class); Assert.assertTrue(fGdbSync != null); IMIProcesses procService = fServicesTracker.getService(IMIProcesses.class); Assert.assertTrue(procService != null); // Register to receive DSF events fSession.addServiceEventListener(ThreadStackFrameSyncTest.this, null); } }; fSession.getExecutor().submit(runnable).get(); } @Override protected void setLaunchAttributes() { super.setLaunchAttributes(); setLaunchAttribute(ICDTLaunchConfigurationConstants.ATTR_PROGRAM_NAME, EXEC_PATH + EXEC_NAME); // Multi run control only makes sense for non-stop mode setLaunchAttribute(IGDBLaunchConfigurationConstants.ATTR_DEBUGGER_NON_STOP, true); } @Override public void doAfterTest() throws Exception { super.doAfterTest(); if (fSession != null) { fSession.getExecutor().submit(() -> fSession.removeServiceEventListener(ThreadStackFrameSyncTest.this)) .get(); } fEventsReceived.clear(); if (fServicesTracker!=null) fServicesTracker.dispose(); } ////////////////////////////////////////////////////////////////////////////////////// // Start of tests ////////////////////////////////////////////////////////////////////////////////////// /** * This test verifies that changing the active thread, in GDB, in CLI, * triggers a GDB notification that a new thread has been selected. */ @Test public void testChangingCurrentThreadCLINotification() throws Throwable { ServiceEventWaitor<MIStoppedEvent> eventWaitor = new ServiceEventWaitor<MIStoppedEvent>(fMultiRunControl.getSession(), MIStoppedEvent.class); // add a breakpoint in main SyncUtil.addBreakpoint(SOURCE_NAME + ":" + getLineForTag("LINE_MAIN_ALL_THREADS_STARTED"), false); // add a breakpoint in thread code SyncUtil.addBreakpoint("36", false); // Run program SyncUtil.resumeAll(); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for first thread to stop eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for second thread to stop eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // *** at this point all 5 threads should be stopped // Try some thread switching - SwitchThreadAndCaptureThreadSwitchedEvent will // capture the "=thread-selected" event and return the newly selected thread // for us to compare to what we ordered for (int i = 0; i < 2; i++) { assertEquals("2",switchThreadAndCaptureThreadSwitchedEvent("2")); assertEquals("3",switchThreadAndCaptureThreadSwitchedEvent("3")); assertEquals("4",switchThreadAndCaptureThreadSwitchedEvent("4")); assertEquals("5",switchThreadAndCaptureThreadSwitchedEvent("5")); assertEquals("1",switchThreadAndCaptureThreadSwitchedEvent("1")); } } /** * This test verifies that changing the active frame, in GDB, in CLI, * triggers a GDB notification that a new frame has been selected. */ @Test public void testChangingCurrentFrameCLINotification() throws Throwable { ServiceEventWaitor<MIStoppedEvent> eventWaitor = new ServiceEventWaitor<MIStoppedEvent>(fMultiRunControl.getSession(), MIStoppedEvent.class); // add a breakpoint in main SyncUtil.addBreakpoint(SOURCE_NAME + ":" + getLineForTag("LINE_MAIN_ALL_THREADS_STARTED"), false); // add a breakpoint in thread code SyncUtil.addBreakpoint("36", false); // Run program SyncUtil.resumeAll(); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for first thread to stop eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for second thread to stop eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // *** at this point all 5 threads should be stopped // switch to a thread that has some stack frames assertEquals("2",switchThreadAndCaptureThreadSwitchedEvent("2")); // Try some stack frame switching - SwitchFrameAndCaptureThreadSwitchedEvent will // capture the "=thread-selected" event and return the newly selected stack frame, // for us to compare to what we ordered for (int i = 0; i < 5; i++) { assertEquals("1",switchFrameAndCaptureStackFrameSwitchedEvent("1")); assertEquals("0",switchFrameAndCaptureStackFrameSwitchedEvent("0")); } } /** * This test verifies that the GDB Synchronizer service is able to set * the current GDB thread */ @Test public void testGdbSyncServiceCanSwitchGDBThread() throws Throwable { ServiceEventWaitor<MIStoppedEvent> eventWaitor = new ServiceEventWaitor<MIStoppedEvent>(fMultiRunControl.getSession(), MIStoppedEvent.class); // add a breakpoint in main SyncUtil.addBreakpoint(SOURCE_NAME + ":" + getLineForTag("LINE_MAIN_ALL_THREADS_STARTED"), false); // add a breakpoint in thread code SyncUtil.addBreakpoint("36", false); // Run program SyncUtil.resumeAll(); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for first thread to stop eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for second thread to stop eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // *** at this point all 5 threads should be stopped // have the sync service set GDB current tid to thread 5 fGdbSync.setFocus(new IDMContext[] {getContextForThreadId(5)}, new ImmediateRequestMonitor()); assertEquals("5", getCurrentThread()); fGdbSync.setFocus(new IDMContext[] {getContextForThreadId(4)}, new ImmediateRequestMonitor()); assertEquals("4", getCurrentThread()); fGdbSync.setFocus(new IDMContext[] {getContextForThreadId(3)}, new ImmediateRequestMonitor()); assertEquals("3", getCurrentThread()); fGdbSync.setFocus(new IDMContext[] {getContextForThreadId(2)}, new ImmediateRequestMonitor()); assertEquals("2", getCurrentThread()); fGdbSync.setFocus(new IDMContext[] {getContextForThreadId(1)}, new ImmediateRequestMonitor()); assertEquals("1", getCurrentThread()); } /** * This test verifies that the GDB Synchronizer service is able to set * the current GDB stack frame */ @Test public void testGdbSyncServiceCanSwitchGDBStackFrame() throws Throwable { ServiceEventWaitor<MIStoppedEvent> eventWaitor = new ServiceEventWaitor<MIStoppedEvent>(fMultiRunControl.getSession(), MIStoppedEvent.class); // add a breakpoint in main SyncUtil.addBreakpoint(SOURCE_NAME + ":" + getLineForTag("LINE_MAIN_ALL_THREADS_STARTED"), false); // add a breakpoint in thread code SyncUtil.addBreakpoint("36", false); // Run program SyncUtil.resumeAll(); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for first thread to stop eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for second thread to stop eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // *** at this point all 5 threads should be stopped final IFrameDMContext frame1 = SyncUtil.getStackFrame(1, 1); final IFrameDMContext frame0 = SyncUtil.getStackFrame(1, 0); // do a few of times for (int i = 0; i < 50; i++) { // have the sync service switch stack frame to 1 fSession.getExecutor().execute(new Runnable() { @Override public void run() { fGdbSync.setFocus(new IDMContext[] {frame1}, new ImmediateRequestMonitor()); } }); Thread.sleep(100); assertEquals("1", getCurrentStackFrameLevel()); // have the sync service switch stack frame to 0 fSession.getExecutor().execute(new Runnable() { @Override public void run() { fGdbSync.setFocus(new IDMContext[] {frame0}, new ImmediateRequestMonitor()); } }); Thread.sleep(100); assertEquals("0", getCurrentStackFrameLevel()); } } ////////////////////////////////////////////////////////////////////////////////////// // End of tests ////////////////////////////////////////////////////////////////////////////////////// // SyncUtil.getExecutionContext() takes the index of the // array of all threads, so it will return a thread off by one. // We compensate for this in this method private IMIExecutionDMContext getContextForThreadId(int tid) throws InterruptedException, ExecutionException, TimeoutException { return SyncUtil.getExecutionContext(tid -1); } /** * This is a wrapper around selectGdbThread(), that waits and captures the * expected "=thread-selected" event, and returns the thread id from it. * @throws Throwable */ private String switchThreadAndCaptureThreadSwitchedEvent(String tid) throws Throwable { Thread.sleep(100); fEventsReceived.clear(); selectGdbThread(tid); IDMContext ctx = waitForEvent(IGDBFocusChangedEvent.class).getDMContext(); if (ctx instanceof IMIExecutionDMContext) { IMIExecutionDMContext execDmc = (IMIExecutionDMContext) ctx; return execDmc.getThreadId(); } else if (ctx instanceof IFrameDMContext) { IMIExecutionDMContext execDmc = DMContexts.getAncestorOfType(ctx, IMIExecutionDMContext.class); return execDmc.getThreadId(); } return "unknown"; } /** * Waits and captures the expected "=thread-selected" event, and returns the frame id from it. * @throws Throwable */ private String switchFrameAndCaptureStackFrameSwitchedEvent(String frameLevel) throws Throwable { IFrameDMContext newFrame = null; Thread.sleep(100); fEventsReceived.clear(); selectGdbStackFrame(frameLevel); Object[] elems = fGdbSync.getFocus(); for (Object elem : elems) { if (elem instanceof IFrameDMContext) { newFrame = (IFrameDMContext)elem; break; } } return newFrame != null ? Integer.toString(newFrame.getLevel()) : null; } /** * Changes the current thread, using the CLI command "thread <tid>" * @param tid: the thread id of the thread to switch-to. If empty, * the command will simply report the current thread. * @return the tid of the (possibly newly) currently selected gdb thread * @throws Exception */ private String sendCLIThread(String tid) throws Exception { IContainerDMContext containerDmc = SyncUtil.getContainerContext(); Query<CLIThreadInfo> query = new Query<CLIThreadInfo>() { @Override protected void execute(DataRequestMonitor<CLIThreadInfo> rm) { fCommandControl.queueCommand(new CLICommand<CLIThreadInfo>(containerDmc,"thread " + tid) { @Override public CLIThreadInfo getResult(MIOutput output) { return new CLIThreadInfo(output); } }, rm); } }; fCommandControl.getExecutor().execute(query); CLIThreadInfo info = query.get(); return info.getCurrentThread(); } private String getCurrentThread() throws Exception { return sendCLIThread(""); } /** * Changes the current stack frame, using the CLI command "frame <level>". Then parses * the output to extract the current frame. * @param level the frame level wanted. If empty, the command will report the current level * @return newly set level. * @throws Exception */ private String sendCLIFrame(String level) throws Exception { IContainerDMContext containerDmc = SyncUtil.getContainerContext(); Query<MIInfo> query = new Query<MIInfo>() { @Override protected void execute(DataRequestMonitor<MIInfo> rm) { fCommandControl.queueCommand(new CLICommand<MIInfo>(containerDmc,"frame " + level) { @Override public CLIThreadInfo getResult(MIOutput output) { return new CLIThreadInfo(output); } }, rm); } }; fCommandControl.getExecutor().execute(query); String frameLevel = null; for (MIOOBRecord oobr : query.get().getMIOutput().getMIOOBRecords()) { // if frame changed, we'll get this printout: if (oobr instanceof MINotifyAsyncOutput) { // example of output: // =thread-selected,id="2",frame={level="1",addr="0x00007ffff7bc4184",func="start_thread",args=[],from="/lib/x86_64-linux-gnu/libpthread.so.0"} MINotifyAsyncOutput out = (MINotifyAsyncOutput) oobr; String miEvent = out.getAsyncClass(); if ("thread-selected".equals(miEvent)) { //$NON-NLS-1$ // parse =thread-selected to extract current stack frame MIResult[] results = out.getMIResults(); for (int i = 0; i < results.length; i++) { String var = results[i].getVariable(); MIValue val = results[i].getMIValue(); if (var.equals("frame") && val instanceof MITuple) { //$NON-NLS-1$ // dig deeper to get the frame level MIResult[] res = ((MITuple)val).getMIResults(); for (int j = 0; j < res.length; j++) { var = res[j].getVariable(); val = res[j].getMIValue(); if (var.equals("level")) { //$NON-NLS-1$ if (val instanceof MIConst) { frameLevel = ((MIConst) val).getString(); } } } } } } } // if frame command was not given a parameter or the parameter is already // the current frame, we'll get this version of the printout: else if (oobr instanceof MIConsoleStreamOutput) { // example of output (here frame = 0): // ~"#0 main (argc=1 ... String printout = ((MIConsoleStreamOutput) oobr).getCString(); int index1 = printout.indexOf('#'); int index2 = printout.indexOf(' '); if (index1 != -1 && index2 != -1) { frameLevel = printout.substring(index1 + 1, index2); break; } } } return frameLevel; } private String getCurrentStackFrameLevel() throws Throwable { return sendCLIFrame(""); } @DsfServiceEventHandler public void eventDispatched(IDMEvent<? extends IDMContext> e) { synchronized(this) { fEventsReceived.add(e); notifyAll(); } } private void selectGdbThread(String tid) throws Throwable { queueConsoleCommand(String.format("thread %s", tid)); } private void selectGdbStackFrame(String frameLevel) throws Throwable { queueConsoleCommand(String.format("frame %s", frameLevel)); } private void queueConsoleCommand(String command) throws Throwable { queueConsoleCommand(command, TestsPlugin.massageTimeout(DEFAULT_TIMEOUT), TimeUnit.MILLISECONDS); } private void queueConsoleCommand(final String command, int timeout, TimeUnit unit) throws Throwable { Query<MIInfo> query = new Query<MIInfo>() { @Override protected void execute(DataRequestMonitor<MIInfo> rm) { fCommandControl.queueCommand( fCommandControl.getCommandFactory().createMIInterpreterExecConsole( fCommandControl.getContext(), command), rm); } }; fSession.getExecutor().execute(query); query.get(timeout, unit); } private <V extends IDMEvent<? extends IDMContext>> V waitForEvent(Class<V> eventType) throws Exception { return waitForEvent(eventType, TestsPlugin.massageTimeout(DEFAULT_TIMEOUT)); } @SuppressWarnings("unchecked") private <V extends IDMEvent<? extends IDMContext>> V waitForEvent(Class<V> eventType, int timeout) throws Exception { IDMEvent<?> event = getEvent(eventType); if (event == null) { synchronized(this) { try { wait(timeout); } catch (InterruptedException ex) { } } event = getEvent(eventType); if (event == null) { throw new Exception(String.format("Timed out waiting for '%s' to occur.", eventType.getName())); } } return (V)event; } @SuppressWarnings("unchecked") private synchronized <V extends IDMEvent<? extends IDMContext>> V getEvent(Class<V> eventType) { for (IDMEvent<?> e : fEventsReceived) { if (eventType.isAssignableFrom(e.getClass())) { fEventsReceived.remove(e); return (V)e; } } return null; } }