/******************************************************************************* * Copyright (c) 2000, 2010 QNX Software Systems 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 * * Contributors: * QNX Software Systems - Initial API and implementation * Norbert Ploett, Siemens AG - fix for bug 119370 * Hewlett-Packard Development Company - fix for bug 109733 (null check in setPrompt) *******************************************************************************/ package org.eclipse.cdt.debug.mi.core; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.StringTokenizer; import org.eclipse.cdt.debug.mi.core.command.CLICommand; import org.eclipse.cdt.debug.mi.core.command.Command; import org.eclipse.cdt.debug.mi.core.command.MIExecContinue; import org.eclipse.cdt.debug.mi.core.command.MIExecFinish; import org.eclipse.cdt.debug.mi.core.command.MIExecNext; import org.eclipse.cdt.debug.mi.core.command.MIExecNextInstruction; import org.eclipse.cdt.debug.mi.core.command.MIExecReturn; import org.eclipse.cdt.debug.mi.core.command.MIExecStep; import org.eclipse.cdt.debug.mi.core.command.MIExecStepInstruction; import org.eclipse.cdt.debug.mi.core.command.MIExecUntil; import org.eclipse.cdt.debug.mi.core.command.MIInterpreterExecConsole; import org.eclipse.cdt.debug.mi.core.event.MIBreakpointHitEvent; import org.eclipse.cdt.debug.mi.core.event.MIErrorEvent; import org.eclipse.cdt.debug.mi.core.event.MIEvent; import org.eclipse.cdt.debug.mi.core.event.MIFunctionFinishedEvent; import org.eclipse.cdt.debug.mi.core.event.MIInferiorExitEvent; import org.eclipse.cdt.debug.mi.core.event.MIInferiorSignalExitEvent; import org.eclipse.cdt.debug.mi.core.event.MILocationReachedEvent; import org.eclipse.cdt.debug.mi.core.event.MIRunningEvent; import org.eclipse.cdt.debug.mi.core.event.MISharedLibEvent; import org.eclipse.cdt.debug.mi.core.event.MISignalEvent; import org.eclipse.cdt.debug.mi.core.event.MISteppingRangeEvent; import org.eclipse.cdt.debug.mi.core.event.MIStoppedEvent; import org.eclipse.cdt.debug.mi.core.event.MIWatchpointScopeEvent; import org.eclipse.cdt.debug.mi.core.event.MIWatchpointTriggerEvent; import org.eclipse.cdt.debug.mi.core.output.MIAsyncRecord; import org.eclipse.cdt.debug.mi.core.output.MIConsoleStreamOutput; import org.eclipse.cdt.debug.mi.core.output.MIConst; import org.eclipse.cdt.debug.mi.core.output.MIExecAsyncOutput; import org.eclipse.cdt.debug.mi.core.output.MILogStreamOutput; import org.eclipse.cdt.debug.mi.core.output.MINotifyAsyncOutput; import org.eclipse.cdt.debug.mi.core.output.MIOOBRecord; import org.eclipse.cdt.debug.mi.core.output.MIOutput; import org.eclipse.cdt.debug.mi.core.output.MIParser; import org.eclipse.cdt.debug.mi.core.output.MIResult; import org.eclipse.cdt.debug.mi.core.output.MIResultRecord; import org.eclipse.cdt.debug.mi.core.output.MIStatusAsyncOutput; import org.eclipse.cdt.debug.mi.core.output.MIStreamRecord; import org.eclipse.cdt.debug.mi.core.output.MITargetStreamOutput; import org.eclipse.cdt.debug.mi.core.output.MIValue; /** * Receiving thread of gdb response output. */ public class RxThread extends Thread { final MISession session; LinkedList<MIStreamRecord> fStreamRecords = new LinkedList<MIStreamRecord>(); CLIProcessor cli; int prompt = 1; // 1 --> Primary prompt "(gdb)"; 2 --> Secondary Prompt ">" boolean fEnableConsole = true; public RxThread(MISession s) { super("MI RX Thread"); //$NON-NLS-1$ session = s; cli = new CLIProcessor(session); } /* * Get the response, parse the output, dispatch for OOB * search for the corresponding token in rxQueue for the ResultRecord. */ public void run() { BufferedReader reader = new BufferedReader(new InputStreamReader(session.getChannelInputStream())); try { String line; while ((line = reader.readLine()) != null) { // TRACING: print the output. if (MIPlugin.DEBUG) { MIPlugin.getDefault().debugLog(line); } if (session.isVerboseModeEnabled()) session.writeToConsole(line + "\n"); //$NON-NLS-1$ setPrompt(line); processMIOutput(line + "\n"); //$NON-NLS-1$ } } catch (IOException e) { //e.printStackTrace(); } // This code should be executed when gdb been abruptly // or unxepectedly killed. This is detected by checking // if the channelInputStream is not null. In normal case // session.terminate() will set the channelInputStream to null. if (session.getChannelInputStream() != null) { Runnable cleanup = new Runnable() { public void run() { // Change the state of the inferior. session.getMIInferior().setTerminated(); session.terminate(); } }; Thread clean = new Thread(cleanup, "GDB Died"); //$NON-NLS-1$ clean.setDaemon(true); clean.start(); } // Clear the queue and notify any command waiting, we are going down. CommandQueue rxQueue = session.getRxQueue(); if (rxQueue != null) { Command[] cmds = rxQueue.clearCommands(); for (int i = 0; i < cmds.length; i++) { synchronized (cmds[i]) { cmds[i].notifyAll(); } } } } void setPrompt(String line) { MIParser parser = session.getMIParser(); prompt = 0; // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=109733 if (line == null || parser == null) return; line = line.trim(); if (line.equals(parser.primaryPrompt)) { prompt = 1; } else if (line.equals(parser.secondaryPrompt)) { prompt = 2; } } public boolean inPrimaryPrompt() { return prompt == 1; } public boolean inSecondaryPrompt() { return prompt == 2; } public void setEnableConsole(boolean enable) { fEnableConsole = enable; } public boolean isEnableConsole() { return fEnableConsole; } /** * Search for the command in the RxQueue, set the MIOutput * and notify() the other end. * Any OOBs are consider like event and dipatch to the * listeners/observers in different thread. */ void processMIOutput(String buffer) { MIOutput response = session.parse(buffer); if (response != null) { List<MIEvent> list = new ArrayList<MIEvent>(); CommandQueue rxQueue = session.getRxQueue(); MIResultRecord rr = response.getMIResultRecord(); if (rr != null) { int id = rr.getToken(); Command cmd = rxQueue.removeCommand(id); // Get a snapshot of the accumulated stream records. We clear // the collection below (with each new Result Command response). MIStreamRecord[] streamRecords = fStreamRecords.toArray(new MIStreamRecord[fStreamRecords.size()]); // Check if the state changed. String state = rr.getResultClass(); if ("running".equals(state)) { //$NON-NLS-1$ int type = 0; // Check the type of command // if it was a step instruction set state stepping if (cmd instanceof MIExecNext) { type = MIRunningEvent.NEXT; } else if (cmd instanceof MIExecNextInstruction) { type = MIRunningEvent.NEXTI; } else if (cmd instanceof MIExecStep) { type = MIRunningEvent.STEP; } else if (cmd instanceof MIExecStepInstruction) { type = MIRunningEvent.STEPI; } else if (cmd instanceof MIExecUntil) { type = MIRunningEvent.UNTIL; } else if (cmd instanceof MIExecFinish) { type = MIRunningEvent.FINISH; } else if (cmd instanceof MIExecReturn) { type = MIRunningEvent.RETURN; } else if (cmd instanceof MIExecContinue) { type = MIRunningEvent.CONTINUE; } else { type = MIRunningEvent.CONTINUE; } session.getMIInferior().setRunning(); MIEvent event = new MIRunningEvent(session, id, type); if (cmd != null && cmd.isQuiet()) event.setPropagate(false); list.add(event); } else if ("exit".equals(state)) { //$NON-NLS-1$ // No need to do anything, terminate() will. session.getMIInferior().setTerminated(); } else if ("connected".equals(state)) { //$NON-NLS-1$ session.getMIInferior().setConnected(); } else if ("error".equals(state)) { //$NON-NLS-1$ if (session.getMIInferior().isRunning()) { session.getMIInferior().setSuspended(); MIEvent event = new MIErrorEvent(session, rr, streamRecords); list.add(event); } } else if ("done".equals(state) && cmd instanceof CLICommand) { //$NON-NLS-1$ // "done" usually mean that gdb returns after some CLI command // The result record may contains informaton specific to oob. // This will happen when CLI-Command is use, for example // doing "run" will block and return a breakpointhit processMIOOBRecord(rr, list); } // Set the accumulate console Stream response.setMIOOBRecords(streamRecords); // Notify the waiting command. // Notify any command waiting for a ResultRecord. if (cmd != null) { // Process the Command line to recognize patterns we may need to fire event. if (cmd instanceof CLICommand) { cli.processSettingChanges((CLICommand)cmd); } else if (cmd instanceof MIInterpreterExecConsole) { cli.processSettingChanges((MIInterpreterExecConsole)cmd); } synchronized (cmd) { cmd.setMIOutput(response); cmd.notifyAll(); } } // Clear the accumulated stream records on each new Result Command response. fStreamRecords.clear(); } else { // Process OOBs MIOOBRecord[] oobs = response.getMIOOBRecords(); for (int i = 0; i < oobs.length; i++) { processMIOOBRecord(oobs[i], list); } // If not waiting for any command results, ensure the stream list doesn't // get too large. See Bug 302927 for more if (rxQueue.isEmpty() && fStreamRecords.size() > 20) fStreamRecords.removeFirst(); } MIEvent[] events = list.toArray(new MIEvent[list.size()]); session.fireEvents(events); } // if response != null } /** * Dispatch a thread to deal with the listeners. */ void processMIOOBRecord(MIOOBRecord oob, List<MIEvent> list) { if (oob instanceof MIAsyncRecord) { processMIOOBRecord((MIAsyncRecord) oob, list); fStreamRecords.clear(); } else if (oob instanceof MIStreamRecord) { processMIOOBRecord((MIStreamRecord) oob); } } void processMIOOBRecord(MIAsyncRecord async, List<MIEvent> list) { if (async instanceof MIExecAsyncOutput) { MIExecAsyncOutput exec = (MIExecAsyncOutput) async; // Change of state. String state = exec.getAsyncClass(); if ("stopped".equals(state)) { //$NON-NLS-1$ MIResult[] results = exec.getMIResults(); for (int i = 0; i < results.length; i++) { String var = results[i].getVariable(); MIValue val = results[i].getMIValue(); if (var.equals("reason")) { //$NON-NLS-1$ if (val instanceof MIConst) { String reason = ((MIConst) val).getString(); MIEvent e = createEvent(reason, exec); if (e != null) { list.add(e); } } } } // GDB does not provide reason when stopping on a shared library // event or because of a catchpoint (in gdb < 7.0). // Hopefully this will be fixed in a future version. Meanwhile, // we will use a hack to cope. On most platform we can detect by // looking at the console stream for phrase. Although it is a // _real_ bad idea to do this, we do not have any other // alternatives. if (list.isEmpty()) { String[] logs = getStreamRecords(); for (int i = 0; i < logs.length; i++) { if (logs[i].equalsIgnoreCase("Stopped due to shared library event")) { //$NON-NLS-1$ session.getMIInferior().setSuspended(); MIEvent e = new MISharedLibEvent(session, exec); list.add(e); } else if (logs[i].startsWith("Catchpoint ")) { //$NON-NLS-1$ session.getMIInferior().setSuspended(); // Example: "Catchpoint 1 (exception caught)" StringTokenizer tokenizer = new StringTokenizer(logs[i]); tokenizer.nextToken(); // "Catchpoint" try { int bkptNumber = Integer.parseInt(tokenizer.nextToken()); // 1 list.add(new MIBreakpointHitEvent(session, exec, bkptNumber)); } catch (NumberFormatException exc) { assert false : "unexpected catchpoint stream record format: " + logs[i]; //$NON-NLS-1$ } } } } // We were stopped for some unknown reason, for example // GDB for temporary breakpoints will not send the // "reason" ??? still fire a stopped event. if (list.isEmpty()) { session.getMIInferior().setSuspended(); MIEvent e = new MIStoppedEvent(session, exec); list.add(e); } } } else if (async instanceof MIStatusAsyncOutput) { // Nothing done .. but what about +download?? } else if (async instanceof MINotifyAsyncOutput) { // Nothing } } void processMIOOBRecord(MIStreamRecord stream) { if (stream instanceof MIConsoleStreamOutput) { OutputStream console = session.getConsolePipe(); if (console != null) { MIConsoleStreamOutput out = (MIConsoleStreamOutput) stream; String str = out.getString(); // Process the console stream too. setPrompt(str); if (str != null && isEnableConsole()) { try { console.write(str.getBytes()); console.flush(); } catch (IOException e) { } } } // Accumulate the Console Stream Output response for parsing. // Some commands will put valuable info in the Console Stream. fStreamRecords.add(stream); } else if (stream instanceof MITargetStreamOutput) { OutputStream target = session.getMIInferior().getPipedOutputStream(); if (target != null) { MITargetStreamOutput out = (MITargetStreamOutput) stream; String str = out.getString(); if (str != null) { try { target.write(str.getBytes()); target.flush(); } catch (IOException e) { } } } // Accumulate the Target Stream Output response for parsing. // Some commands, e.g. 'monitor' will put valuable info in the Console Stream. // This fixes bug 119370. fStreamRecords.add(stream); } else if (stream instanceof MILogStreamOutput) { // This is meant for the gdb console. OutputStream log = session.getLogPipe(); if (log != null) { MILogStreamOutput out = (MILogStreamOutput) stream; String str = out.getString(); if (str != null && isEnableConsole()) { try { log.write(str.getBytes()); log.flush(); } catch (IOException e) { } } } // Accumulate the Log Stream Output response for parsing. // Some commands will put valuable info in the Log Stream. fStreamRecords.add(stream); } } /** * Check for any info that we can gather form the console. */ void processMIOOBRecord(MIResultRecord rr, List<MIEvent> list) { MIResult[] results = rr.getMIResults(); for (int i = 0; i < results.length; i++) { String var = results[i].getVariable(); if (var.equals("reason")) { //$NON-NLS-1$ MIValue value = results[i].getMIValue(); if (value instanceof MIConst) { String reason = ((MIConst) value).getString(); MIEvent event = createEvent(reason, rr); if (event != null) { list.add(event); } } } } // GDB does not have reason when stopping on shared, hopefully // this will be fix in newer version meanwhile, we will use a hack // to cope. On most platform we can detect this state by looking at the // console stream for the phrase: // ~"Stopped due to shared library event\n" // // Althought it is a _real_ bad idea to do this, we do not have // any other alternatives. if (list.isEmpty()) { String[] logs = getStreamRecords(); for (int i = 0; i < logs.length; i++) { if (logs[i].equalsIgnoreCase("Stopped due to shared library event")) { //$NON-NLS-1$ session.getMIInferior().setSuspended(); MIEvent e = new MISharedLibEvent(session, rr); list.add(e); } } } // We were stopped for some unknown reason, for example // GDB for temporary breakpoints will not send the // "reason" ??? still fire a stopped event. if (list.isEmpty()) { if (session.getMIInferior().isRunning()) { session.getMIInferior().setSuspended(); MIEvent event = new MIStoppedEvent(session, rr); session.fireEvent(event); } } } MIEvent createEvent(String reason, MIExecAsyncOutput exec) { return createEvent(reason, null, exec); } MIEvent createEvent(String reason, MIResultRecord rr) { return createEvent(reason, rr, null); } MIEvent createEvent(String reason, MIResultRecord rr, MIExecAsyncOutput exec) { MIEvent event = null; if ("breakpoint-hit".equals(reason)) { //$NON-NLS-1$ if (exec != null) { event = new MIBreakpointHitEvent(session, exec); } else if (rr != null) { event = new MIBreakpointHitEvent(session, rr); } session.getMIInferior().setSuspended(); } else if ( "watchpoint-trigger".equals(reason) //$NON-NLS-1$ || "read-watchpoint-trigger".equals(reason) //$NON-NLS-1$ || "access-watchpoint-trigger".equals(reason)) { //$NON-NLS-1$ if (exec != null) { event = new MIWatchpointTriggerEvent(session, exec); } else if (rr != null) { event = new MIWatchpointTriggerEvent(session, rr); } session.getMIInferior().setSuspended(); } else if ("watchpoint-scope".equals(reason)) { //$NON-NLS-1$ if (exec != null) { event = new MIWatchpointScopeEvent(session, exec); } else if (rr != null) { event = new MIWatchpointScopeEvent(session, rr); } session.getMIInferior().setSuspended(); } else if ("end-stepping-range".equals(reason)) { //$NON-NLS-1$ if (exec != null) { event = new MISteppingRangeEvent(session, exec); } else if (rr != null) { event = new MISteppingRangeEvent(session, rr); } session.getMIInferior().setSuspended(); } else if ("signal-received".equals(reason)) { //$NON-NLS-1$ if (exec != null) { event = new MISignalEvent(session, exec); } else if (rr != null) { event = new MISignalEvent(session, rr); } session.getMIInferior().setSuspended(); } else if ("location-reached".equals(reason)) { //$NON-NLS-1$ if (exec != null) { event = new MILocationReachedEvent(session, exec); } else if (rr != null) { event = new MILocationReachedEvent(session, rr); } session.getMIInferior().setSuspended(); } else if ("function-finished".equals(reason)) { //$NON-NLS-1$ if (exec != null) { event = new MIFunctionFinishedEvent(session, exec); } else if (rr != null) { event = new MIFunctionFinishedEvent(session, rr); } session.getMIInferior().setSuspended(); } else if ("exited-normally".equals(reason) || "exited".equals(reason)) { //$NON-NLS-1$ //$NON-NLS-2$ if (exec != null) { event = new MIInferiorExitEvent(session, exec); } else if (rr != null) { event = new MIInferiorExitEvent(session, rr); } session.getMIInferior().setTerminated(0,false); } else if ("exited-signalled".equals(reason)) { //$NON-NLS-1$ if (exec != null) { event = new MIInferiorSignalExitEvent(session, exec); } else if (rr != null) { event = new MIInferiorSignalExitEvent(session, rr); } session.getMIInferior().setTerminated(0,false); } else if ("shlib-event".equals(reason)) { //$NON-NLS-1$ if (exec != null) { event = new MISharedLibEvent(session, exec); } else if (rr != null) { event = new MISharedLibEvent(session, rr); } session.getMIInferior().setSuspended(); } return event; } String[] getStreamRecords() { List<String> streamRecords = new ArrayList<String>(fStreamRecords.size()); for (MIStreamRecord rec : fStreamRecords) { String str = rec.getString().trim(); if (str.length() > 0) { streamRecords.add(str); } } return streamRecords.toArray(new String[streamRecords.size()]); } }