/******************************************************************************* * Copyright (c) 2009, 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 * Wind River Systems - Modified for new DSF Reference Implementation * Ericsson - Modified for additional features in DSF Reference implementation *******************************************************************************/ package org.eclipse.cdt.dsf.mi.service.command; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.util.concurrent.RejectedExecutionException; import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor; import org.eclipse.cdt.dsf.concurrent.DsfRunnable; import org.eclipse.cdt.dsf.concurrent.ThreadSafe; import org.eclipse.cdt.dsf.datamodel.IDMContext; import org.eclipse.cdt.dsf.debug.service.command.ICommand; import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService; import org.eclipse.cdt.dsf.debug.service.command.ICommandListener; import org.eclipse.cdt.dsf.debug.service.command.ICommandResult; import org.eclipse.cdt.dsf.debug.service.command.ICommandToken; import org.eclipse.cdt.dsf.debug.service.command.IEventListener; import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin; import org.eclipse.cdt.dsf.mi.service.command.commands.CLICommand; import org.eclipse.cdt.dsf.mi.service.command.commands.MICommand; import org.eclipse.cdt.dsf.mi.service.command.commands.MIInterpreterExecConsole; import org.eclipse.cdt.dsf.mi.service.command.commands.RawCommand; import org.eclipse.cdt.dsf.mi.service.command.output.MIConsoleStreamOutput; import org.eclipse.cdt.dsf.mi.service.command.output.MIInfo; import org.eclipse.cdt.dsf.mi.service.command.output.MILogStreamOutput; 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.service.DsfSession; import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; /** * This Process implementation tracks the process the GDB process. This * process object is displayed in Debug view and is used to * accept CLI commands and to write their output to the console. * * @see org.eclipse.debug.core.model.IProcess */ @ThreadSafe public abstract class AbstractCLIProcess extends Process implements IEventListener, ICommandListener { public static final String PRIMARY_PROMPT = "(gdb)"; //$NON-NLS-1$ public static final String SECONDARY_PROMPT = ">"; //$NON-NLS-1$ // This is the command that will end a secondary prompt private static final String SECONDARY_PROMPT_END_COMMAND = "end"; //$NON-NLS-1$ private final DsfSession fSession; private final ICommandControlService fCommandControl; private final OutputStream fOutputStream = new CLIOutputStream(); // Client process console stream. private PipedInputStream fMIInConsolePipe; private PipedOutputStream fMIOutConsolePipe; private PipedInputStream fMIInLogPipe; private PipedOutputStream fMIOutLogPipe; private boolean fDisposed = false; /** * Counter for tracking console commands sent by services. * * Services may issue console commands when the available MI commands are * not sufficient. However, these commands may produce console and log * output which should not be written to the user CLI terminal. * * This counter is incremented any time a console command is seen which was * not generated by this class. It is decremented whenever a service CLI * command is finished. When counter value is 0, the CLI process writes * the console output. */ private int fSuppressConsoleOutputCounter = 0; // Primary prompt == "(gdb)" // Secondary Prompt == ">" // Secondary_prompt_missing means that the backend should be sending the secondary // prompt but it isn't. So we do it ourselves. private enum PromptType { IN_PRIMARY_PROMPT, IN_SECONDARY_PROMPT, IN_SECONDARY_PROMPT_MISSING }; private PromptType fPrompt = PromptType.IN_PRIMARY_PROMPT; /** * @since 1.1 */ @ConfinedToDsfExecutor("fSession#getExecutor") public AbstractCLIProcess(ICommandControlService commandControl) throws IOException { fSession = commandControl.getSession(); fCommandControl = commandControl; commandControl.addEventListener(this); commandControl.addCommandListener(this); PipedInputStream miInConsolePipe = null; PipedOutputStream miOutConsolePipe = null; PipedInputStream miInLogPipe = null; PipedOutputStream miOutLogPipe = null; try { // Using a LargePipedInputStream see https://bugs.eclipse.org/bugs/show_bug.cgi?id=223154 miOutConsolePipe = new PipedOutputStream(); miInConsolePipe = new LargePipedInputStream(miOutConsolePipe); miOutLogPipe = new PipedOutputStream(); miInLogPipe = new LargePipedInputStream(miOutLogPipe); } catch (IOException e) { ILog log = GdbPlugin.getDefault().getLog(); if (log != null) { log.log(new Status( IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1, "Error when creating log pipes", e)); //$NON-NLS-1$ } } // Must initialize these outside of the try block because they are final. fMIOutConsolePipe = miOutConsolePipe; fMIInConsolePipe = miInConsolePipe; fMIOutLogPipe = miOutLogPipe; fMIInLogPipe = miInLogPipe; } protected DsfSession getSession() { return fSession; } /** * @since 1.1 */ protected ICommandControlService getCommandControlService() { return fCommandControl; } protected boolean isDisposed() { return fDisposed; } @ConfinedToDsfExecutor("fSession#getExecutor") public void dispose() { if (fDisposed) return; fCommandControl.removeEventListener(this); fCommandControl.removeCommandListener(this); closeIO(); fDisposed = true; // We have memory leaks that prevent this class from being // GCed. The problem becomes bad because we are holding // two LargePipedInputStream and eventually, the JUnit tests // run out of memory. To address this particular problem, // before the actual causes of the leaks are fixed, lets // make sure we release all our four streams which all have // a reference to a LargePipedInputStream // Bug 323071 fMIInConsolePipe = null; fMIInLogPipe = null; fMIOutConsolePipe = null; fMIOutLogPipe = null; } private void closeIO() { try { fMIOutConsolePipe.close(); } catch (IOException e) {} try { fMIInConsolePipe.close(); } catch (IOException e) {} try { fMIOutLogPipe.close(); } catch (IOException e) {} try { fMIInLogPipe.close(); } catch (IOException e) {} } /** * @see java.lang.Process#getErrorStream() */ @Override public InputStream getErrorStream() { return fMIInLogPipe; } /** * @see java.lang.Process#getInputStream() */ @Override public InputStream getInputStream() { return fMIInConsolePipe; } /** * @see java.lang.Process#getOutputStream() */ @Override public OutputStream getOutputStream() { return fOutputStream; } public void eventReceived(Object output) { if (fSuppressConsoleOutputCounter > 0) return; for (MIOOBRecord oobr : ((MIOutput)output).getMIOOBRecords()) { if (oobr instanceof MIConsoleStreamOutput) { MIConsoleStreamOutput out = (MIConsoleStreamOutput) oobr; String str = out.getString(); if (str.trim().equals(SECONDARY_PROMPT)) { // Make sure to skip any secondary prompt that we // have already printed ourselves. This would happen // when a new version of the backend starts sending // the secondary prompt for a command that it didn't // use to. In this case, we still send it ourselves. if (inMissingSecondaryPrompt()) { return; } // Add a space for readability str = SECONDARY_PROMPT + ' '; } setPrompt(str); try { if (fMIOutConsolePipe != null) { fMIOutConsolePipe.write(str.getBytes()); fMIOutConsolePipe.flush(); } } catch (IOException e) { } } else if (oobr instanceof MILogStreamOutput) { MILogStreamOutput out = (MILogStreamOutput) oobr; String str = out.getString(); if (str != null) { try { if (fMIOutLogPipe != null) { fMIOutLogPipe.write(str.getBytes()); fMIOutLogPipe.flush(); } } catch (IOException e) { } } } } } public void commandQueued(ICommandToken token) { // Ignore } public void commandSent(ICommandToken token) { // Bug 285170 // Don't reset the fPrompt here, in case we are // dealing with the missing secondary prompt. ICommand<?> command = token.getCommand(); // Check if the command is a CLI command and if it did not originate from this class. if (command instanceof CLICommand<?> && !(command instanceof ProcessCLICommand || command instanceof ProcessMIInterpreterExecConsole)) { fSuppressConsoleOutputCounter++; } // Bug 285170 // Deal with missing secondary prompt, if needed. // The only two types we care about are ProcessMIInterpreterExecConsole // and RawCommand, both of which are MICommands if (command instanceof MICommand<?>) { checkMissingSecondaryPrompt((MICommand<?>)command); } } private void checkMissingSecondaryPrompt(MICommand<?> command) { // If the command send is one of ours, check if it is one that is missing a secondary prompt if (command instanceof ProcessMIInterpreterExecConsole) { String[] operations = ((ProcessMIInterpreterExecConsole)command).getParameters(); if (operations != null && operations.length > 0) { // Get the command name. String operation = operations[0]; int indx = operation.indexOf(' '); if (indx != -1) { operation = operation.substring(0, indx).trim(); } else { operation = operation.trim(); } if (isMissingSecondaryPromptCommand(operation)) { // For such commands, the backend does not send the secondary prompt // so we set it manually. We'll remain in this state until we get // a commandDone() call. // This logic will still work when a new version of the backend // fixes this lack of secondary prompt. fPrompt = PromptType.IN_SECONDARY_PROMPT_MISSING; } } } // Even if the previous check didn't kick in, we may already be in the missing // secondary prompt case. If so, we'll print the prompt ourselves. // Just make sure that this command is not ending the secondary prompt. if (fPrompt == PromptType.IN_SECONDARY_PROMPT_MISSING) { String operation = command.getOperation(); if (operation != null) { int indx = operation.indexOf(' '); if (indx != -1) { operation = operation.substring(0, indx).trim(); } else { operation = operation.trim(); } if (!operation.equals(SECONDARY_PROMPT_END_COMMAND)) { // Add a space for readability String str = SECONDARY_PROMPT + ' '; try { if (fMIOutConsolePipe != null) { fMIOutConsolePipe.write(str.getBytes()); fMIOutConsolePipe.flush(); } } catch (IOException e) { } } } } } /** * Check to see if the user typed a command that we know the backend * does not send the secondary prompt for, but should. * If so, we'll need to pretend we are receiving the secondary prompt. * * @since 3.0 */ protected boolean isMissingSecondaryPromptCommand(String operation) { return false; } public void commandRemoved(ICommandToken token) { // Ignore } public void commandDone(ICommandToken token, ICommandResult result) { // Whenever we get a command that is completed, we know we must be in the primary prompt fPrompt = PromptType.IN_PRIMARY_PROMPT; ICommand<?> command = token.getCommand(); if (token.getCommand() instanceof CLICommand<?> && !(command instanceof ProcessCLICommand || command instanceof ProcessMIInterpreterExecConsole)) { fSuppressConsoleOutputCounter--; } } void setPrompt(String line) { fPrompt = PromptType.IN_PRIMARY_PROMPT; // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=109733 if (line == null) return; line = line.trim(); if (line.equals(SECONDARY_PROMPT)) { fPrompt = PromptType.IN_SECONDARY_PROMPT; } } public boolean inPrimaryPrompt() { return fPrompt == PromptType.IN_PRIMARY_PROMPT; } public boolean inSecondaryPrompt() { return fPrompt == PromptType.IN_SECONDARY_PROMPT || fPrompt == PromptType.IN_SECONDARY_PROMPT_MISSING; } /** * @since 3.0 */ public boolean inMissingSecondaryPrompt() { return fPrompt == PromptType.IN_SECONDARY_PROMPT_MISSING; } private boolean isMIOperation(String operation) { // The definition of an MI command states that it starts with // [ token ] "-" // where 'token' is optional and a sequence of digits. // However, we don't accept a token from the user, because // we will be adding our own token when actually sending the command. if (operation.startsWith("-")) { //$NON-NLS-1$ return true; } return false; } private class CLIOutputStream extends OutputStream { private final StringBuffer buf = new StringBuffer(); @Override public void write(int b) throws IOException { buf.append((char)b); if (b == '\n') { // Throw away the newline. final String bufString = buf.toString().trim(); buf.setLength(0); try { fSession.getExecutor().execute(new DsfRunnable() { public void run() { try { post(bufString); } catch (IOException e) { // Pipe closed. } }}); } catch (RejectedExecutionException e) { // Session disposed. } } } // Encapsulate the string sent to gdb in a fake // command and post it to the TxThread. public void post(String str) throws IOException { if (isDisposed()) return; ICommand<MIInfo> cmd = null; // 1- // if We have the secondary prompt it means // that GDB is waiting for more feedback, use a RawCommand // 2- // Do not use the interpreter-exec for stepping operation // the UI will fall out of step. // Also, do not use "interpreter-exec console" for MI commands. // 3- // Normal Command Line Interface. boolean secondary = inSecondaryPrompt(); if (secondary) { cmd = new RawCommand(getCommandControlService().getContext(), str); } else if (! isMIOperation(str) && ! CLIEventProcessor.isSteppingOperation(str)) { cmd = new ProcessMIInterpreterExecConsole(getCommandControlService().getContext(), str); } else { cmd = new ProcessCLICommand(getCommandControlService().getContext(), str); } final ICommand<MIInfo> finalCmd = cmd; fSession.getExecutor().execute(new DsfRunnable() { public void run() { if (isDisposed()) return; // Do not wait around for the answer. getCommandControlService().queueCommand(finalCmd, null); }}); } } private class ProcessCLICommand extends CLICommand<MIInfo> { public ProcessCLICommand(IDMContext ctx, String oper) { super(ctx, oper); } } private class ProcessMIInterpreterExecConsole extends MIInterpreterExecConsole<MIInfo> { public ProcessMIInterpreterExecConsole(IDMContext ctx, String cmd) { super(ctx, cmd); } } }