/******************************************************************************* * Copyright (c) 2007, 2016 Wind River 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: * Wind River Systems - initial API and implementation * Ericsson - Modified for caching commands corresponding to multiple execution contexts *******************************************************************************/ package org.eclipse.cdt.dsf.debug.service.command; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor; import org.eclipse.cdt.dsf.concurrent.DsfRunnable; import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants; import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor; import org.eclipse.cdt.dsf.datamodel.DMContexts; import org.eclipse.cdt.dsf.datamodel.IDMContext; import org.eclipse.cdt.dsf.internal.DsfPlugin; import org.eclipse.cdt.dsf.service.DsfSession; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; /** * This is a utility class for caching results of commands--typically commands * sent to an external engine via some protocol (GDB/MI, e.g.). Commands are * sent to their intended target through the supplied {@link ICommandControl}, * and their results are cached so as to quickly service future identical * commands. * * The cache has to be reset by the client that owns and uses the cache. A reset * should be done when an event occurs that alters the state of a context such * that commands on that context may no longer yield the same outcome as they * did before the event. A reset can be done on the entire cache or on a per * context basis. * * @since 1.0 */ public class CommandCache implements ICommandListener { static enum CommandStyle { COALESCED, NONCOALESCED } /** * Holds cache information for a given command. * @param <V> Type matches the result type associated with the command. */ class CommandInfo { /* * Control variables. */ /** List of the request monitors associated with this command */ private final List<DataRequestMonitor<ICommandResult>> fCurrentRequestMonitors ; /** Original command. Need for reference from Queue completion notification */ private final ICommand<ICommandResult> fCommand; /** Style of this command ( internal coalesced or not) */ private final CommandStyle fCmdStyle; /** Command being processed for this command */ private CommandInfo fCoalescedCmd; private ICommandToken fToken; public CommandInfo(CommandStyle cmdstyle, ICommand<ICommandResult> cmd, DataRequestMonitor<ICommandResult> rm ) { fCmdStyle = cmdstyle; fCommand = cmd; fCurrentRequestMonitors = new LinkedList<DataRequestMonitor<ICommandResult>>(); fCurrentRequestMonitors.add(rm); fCoalescedCmd = null; } public CommandStyle getCommandstyle() { return fCmdStyle; } public List<DataRequestMonitor<ICommandResult>> getRequestMonitorList() { return fCurrentRequestMonitors; } public ICommand<ICommandResult> getCommand() { return fCommand; } public CommandInfo getCoalescedCmd() { return fCoalescedCmd; } public void setCoalescedCmd( CommandInfo cmd ) { fCoalescedCmd = cmd; } @Override public boolean equals(Object other) { if (!(other instanceof CommandInfo)) return false; CommandInfo otherCmd = (CommandInfo)other; return otherCmd.fCommand.equals(fCommand); } @Override public int hashCode() { return fCommand.hashCode(); } } class CommandResultInfo { private final ICommandResult fData; private final IStatus fStatus; public CommandResultInfo(ICommandResult data, IStatus status) { fData = data; fStatus = status; } public ICommandResult getData() { return fData; } public IStatus getStatus() { return fStatus; } } private DsfSession fSession; /** * The command control to be used to send commands to the backend. * In certain cases, a {@link BufferedCommandControl} should be used * to artificially delay the processing of command results; these * artificial delays are important to keep events and command results * ordered as received by the backend. */ private ICommandControl fCommandControl; /* * This class contains 5 significant lists. * * Cached Results : * * Contains a mapping of commands and their completed results. Until the cached * results are cleared by the owner of the cache. * * Pending Commands Not Queued : * * The Control object has not yet indicated that it has recognized the command * yet. The user is not allowed to interrogate these objects until the Control * object indicates they have been queued ( commandQueued notification ). * * Pending Commands Unsent : * * This is the list of commands which have been issued to the Control object but * have not been actually issued to the backend. These commands represent coalesce * options. They may be compared against the Queued list being maintained by the * Control object until told otherwise - commandSent notification ). * * Pending Commands Sent : * * This is a list of commands which have been issued to the Control object and * have also been sent to the backend. It is not possible use these objects for * coalescents. * * Coalesced Pending Q : * * These represent original commands for which a new coalesced command has been * created. When the coalesced commands completes the results will be decomposed * when back into individual results from this command. */ private Set<IDMContext> fAvailableContexts = new HashSet<IDMContext>(); private Map<IDMContext, HashMap<CommandInfo, CommandResultInfo>> fCachedContexts = new HashMap<IDMContext, HashMap<CommandInfo, CommandResultInfo>>(); private ArrayList<CommandInfo> fPendingQCommandsSent = new ArrayList<CommandInfo>(); private ArrayList<CommandInfo> fPendingQCommandsNotYetSent = new ArrayList<CommandInfo>(); private ArrayList<CommandInfo> fPendingQWaitingForCoalescedCompletion = new ArrayList<CommandInfo>(); private static boolean DEBUG = false; private static final String CACHE_TRACE_IDENTIFIER = " [CHE]"; //$NON-NLS-1$ private static String BLANK_CACHE_TRACE_IDENTIFIER = ""; //$NON-NLS-1$ static { DEBUG = Boolean.parseBoolean(Platform.getDebugOption("org.eclipse.cdt.dsf/debugCache")); //$NON-NLS-1$ for (int i=0; i<CACHE_TRACE_IDENTIFIER.length(); i++) { BLANK_CACHE_TRACE_IDENTIFIER += " "; //$NON-NLS-1$ } } private void debug(String message) { debug(message, ""); //$NON-NLS-1$ } private void debug(String message, String prefix) { if (DEBUG) { // The message can span more than one line String[] multiLine = message.split("\n"); //$NON-NLS-1$ // Create a blank prefix for proper alignment String blankPrefix = ""; //$NON-NLS-1$ for (int i=0; i<prefix.length(); i++) { blankPrefix += " "; //$NON-NLS-1$ } for (int i = 0; i < multiLine.length; i++) { String traceIdentifier; if (i == 0) { // For the first line we prepend the cache identifier string traceIdentifier = CACHE_TRACE_IDENTIFIER + prefix; } else { // For all other lines we prepend a blank prefix for proper alignment traceIdentifier = BLANK_CACHE_TRACE_IDENTIFIER + blankPrefix; } message = DsfPlugin.getDebugTime() + traceIdentifier + " " + multiLine[i]; //$NON-NLS-1$ // Make sure our lines are not too long while (message.length() > 100) { String partial = message.substring(0, 100) + "\\"; //$NON-NLS-1$ message = message.substring(100); System.out.println(partial); } System.out.println(message); } } } public CommandCache(DsfSession session, ICommandControl control) { fSession = session; fCommandControl = control; /* * We listen for the notifications that the commands have been sent to * their intended target via the ICommandControl service. */ fCommandControl.addCommandListener(this); } /* * Constructs a coalesced command if possible. */ private CommandInfo getCoalescedCommand(CommandInfo cmd) { for ( CommandInfo currentUnsentEntry : new ArrayList<CommandInfo>(fPendingQCommandsNotYetSent) ) { /* * Get the current unsent entry to determine if we can coalesced with it. */ ICommand<?> unsentCommand = currentUnsentEntry.getCommand(); /* * Check if we can so construct a new COALESCED command from scratch. */ // For sanity's sake, cast the generic ?'s to concrete types in the cache implementation. @SuppressWarnings("unchecked") ICommand<ICommandResult> coalescedCmd = (ICommand<ICommandResult>)unsentCommand.coalesceWith( cmd.getCommand() ); if ( coalescedCmd != null ) { CommandInfo coalescedCmdInfo = new CommandInfo( CommandStyle.COALESCED, coalescedCmd, null) ; if ( currentUnsentEntry.getCommandstyle() == CommandStyle.COALESCED ) { /* * We matched a command which is itself already a COALESCED command. So * we need to run through the reference list and point all the current * command which are referencing the command we just subsumed and change * them to point to the new super command. */ for ( CommandInfo waitingEntry : new ArrayList<CommandInfo>(fPendingQWaitingForCoalescedCompletion) ) { if ( waitingEntry.getCoalescedCmd() == currentUnsentEntry ) { /* * This referenced the old command change it to point to the new one. */ waitingEntry.setCoalescedCmd(coalescedCmdInfo); } } } else { /* * This currently unsent entry needs to go into the coalescing list. To * be completed when the coalesced command comes back with a result. */ fPendingQWaitingForCoalescedCompletion.add(currentUnsentEntry); currentUnsentEntry.setCoalescedCmd(coalescedCmdInfo); } /* * Either way we want to take the command back from the Control object so it * does not continue to process it. */ fPendingQCommandsNotYetSent.remove(currentUnsentEntry); fCommandControl.removeCommand(currentUnsentEntry.fToken); return( coalescedCmdInfo ); } } return null; } /** * Executes given ICommand, or retrieves the cached result if known. * @param command Command to execute. * @param rm Return token, contains the retrieved MIInfo object as * well as its cache status. */ public <V extends ICommandResult> void execute(ICommand<V> command, DataRequestMonitor<V> rm) { assert fSession.getExecutor().isInExecutorThread(); // Cast the generic ?'s to concrete types in the cache implementation. @SuppressWarnings("unchecked") final ICommand<ICommandResult> genericCommand = (ICommand<ICommandResult>)command; @SuppressWarnings("unchecked") final DataRequestMonitor<ICommandResult> genericDone = (DataRequestMonitor<ICommandResult>) rm; CommandInfo cachedCmd = new CommandInfo( CommandStyle.NONCOALESCED, genericCommand, genericDone) ; final IDMContext context = genericCommand.getContext(); /* * If command is already cached, just return the cached data. */ if(fCachedContexts.get(context) != null && fCachedContexts.get(context).containsKey(cachedCmd)){ CommandResultInfo result = fCachedContexts.get(context).get(cachedCmd); debug(command.toString().trim()); if (result.getStatus().getSeverity() <= IStatus.INFO) { @SuppressWarnings("unchecked") V v = (V)result.getData(); rm.setData(v); debug(v.toString()); } else { rm.setStatus(result.getStatus()); debug(result.getStatus().toString()); } rm.done(); return; } /* * Return an error if the target is available anymore. */ if (!isTargetAvailable(command.getContext())) { debug(command.toString().trim(), "[N/A]"); //$NON-NLS-1$ rm.setStatus(new Status(IStatus.ERROR, DsfPlugin.PLUGIN_ID, IDsfStatusConstants.INVALID_STATE, "Target not available.", null)); //$NON-NLS-1$ rm.done(); return; } /* * If we are already waiting for this command to complete, * add this request monitor to list of waiting monitors. */ for ( CommandInfo sentCommand : fPendingQCommandsSent ) { if ( sentCommand.equals( cachedCmd )) { sentCommand.getRequestMonitorList().add(genericDone); debug(command.toString().trim(), "[SNT]"); //$NON-NLS-1$ return; } } for ( CommandInfo notYetSentCommand : fPendingQCommandsNotYetSent ) { if ( notYetSentCommand.equals( cachedCmd )) { notYetSentCommand.getRequestMonitorList().add(genericDone); debug(command.toString().trim(), "[SND]"); //$NON-NLS-1$ return; } } /* * We see if this command can be combined into a coalesced one. The * coalesce routine will take care of the already enqueued one which * this command is being coalesced with. */ CommandInfo coalescedCmd = getCoalescedCommand(cachedCmd); if ( coalescedCmd != null ) { /* * The original command we were handed needs to go into the waiting QUEUE. * We also need to point it it to the coalesced command. */ fPendingQWaitingForCoalescedCompletion.add(cachedCmd); cachedCmd.setCoalescedCmd(coalescedCmd); cachedCmd = coalescedCmd; } /* * Now we have a command to send ( coalesced or not ). Put it in the cannot touch * it list and give it to the Control object. Our state handlers will move it into * the proper list as the Control object deals with it. */ final CommandInfo finalCachedCmd = cachedCmd; fPendingQCommandsNotYetSent.add(finalCachedCmd); finalCachedCmd.fToken = fCommandControl.queueCommand( finalCachedCmd.getCommand(), new DataRequestMonitor<ICommandResult>(ImmediateExecutor.getInstance(), null) { @Override public synchronized void done() { // protect against the cache being called in non-session thread, but at // the same time avoid adding extra dispatch cycles to command processing. if (fSession.getExecutor().isInExecutorThread()) {; super.done(); } else { fSession.getExecutor().execute(new DsfRunnable() { @Override public void run() { superDone(); } }); } } private void superDone() { super.done(); } @Override public void handleCompleted() { /* * Match this up with a command set we know about. */ if ( ! fPendingQCommandsSent.remove(finalCachedCmd) ) { /* * This can happen if the call to queueCommand() completes without * actually having to send the command. In that case, we should find * our command in the fPendingQCommandsNotYetSent queue. * For example, upon termination some queueCommand() implementation * may complete immediately with an error status. Bug 309309 */ if ( ! fPendingQCommandsNotYetSent.remove(finalCachedCmd) ) { assert false : "Missing command"; //$NON-NLS-1$ return; } } ICommandResult result = getData(); IStatus status = getStatus(); if ( finalCachedCmd.getCommandstyle() == CommandStyle.COALESCED ) { /* * We matched a command which is itself already a COALESCED command. So * we need to go through the list of unsent commands which were not sent * because the coalesced command represented it. For each match we find * we create a new result from the coalesced command for it. */ for ( CommandInfo waitingEntry : new ArrayList<CommandInfo>(fPendingQWaitingForCoalescedCompletion) ) { if ( waitingEntry.getCoalescedCmd() == finalCachedCmd ) { /* * Remove this entry from the list since we can complete it. */ fPendingQWaitingForCoalescedCompletion.remove(waitingEntry); // Cast the calculated result back to the requested type. @SuppressWarnings("unchecked") V subResult = (V)result.getSubsetResult(waitingEntry.getCommand()); CommandResultInfo subResultInfo = new CommandResultInfo(subResult, status); if(fCachedContexts.get(context) != null){ fCachedContexts.get(context).put(waitingEntry, subResultInfo); } else { HashMap<CommandInfo, CommandResultInfo> map = new HashMap<CommandInfo, CommandResultInfo>(); map.put(waitingEntry, subResultInfo); fCachedContexts.put(context, map); } if (!isSuccess()) { /* * We had some form of error with the original command. So notify the * original requesters of the issues. */ for (DataRequestMonitor<?> pendingRM : waitingEntry.getRequestMonitorList()) { pendingRM.setStatus(status); pendingRM.done(); } } else { assert subResult != null; /* * Notify the original requesters of the positive results. */ for (DataRequestMonitor<? extends ICommandResult> pendingRM : waitingEntry.getRequestMonitorList()) { // Cast the pending return token to match the requested type. @SuppressWarnings("unchecked") DataRequestMonitor<V> vPendingRM = (DataRequestMonitor<V>) pendingRM; vPendingRM.setData(subResult); vPendingRM.done(); } } } } } else { // Save the command result in cache, but only if the command's context // is still available. Otherwise an error may get cached incorrectly. if (isTargetAvailable(context)) { CommandResultInfo resultInfo = new CommandResultInfo(result, status); if (fCachedContexts.get(context) != null){ fCachedContexts.get(context).put(finalCachedCmd, resultInfo); } else { HashMap<CommandInfo, CommandResultInfo> map = new HashMap<CommandInfo, CommandResultInfo>(); map.put(finalCachedCmd, resultInfo); fCachedContexts.put(context, map); } } // This is an original request which completed. Indicate success or // failure to the original requesters. if (!isSuccess()) { /* * We had some form of error with the original command. So notify the * original requesters of the issues. */ for (DataRequestMonitor<?> pendingRM : finalCachedCmd.getRequestMonitorList()) { pendingRM.setStatus(status); pendingRM.done(); } } else { // Cast the calculated result back to the requested type. @SuppressWarnings("unchecked") V vResult = (V)result; for (DataRequestMonitor<? extends ICommandResult> pendingRM : finalCachedCmd.getRequestMonitorList()) { // Cast the pending return token to match the requested type. @SuppressWarnings("unchecked") DataRequestMonitor<V> vPendingRM = (DataRequestMonitor<V>) pendingRM; vPendingRM.setData(vResult); vPendingRM.done(); } } } } }); } /** * TODO */ public void setContextAvailable(IDMContext context, boolean isAvailable) { if (isAvailable) { fAvailableContexts.add(context); } else { fAvailableContexts.remove(context); for (Iterator<IDMContext> itr = fAvailableContexts.iterator(); itr.hasNext();) { if (DMContexts.isAncestorOf(context, itr.next())) { itr.remove(); } } } } /** * TODO * @see #setContextAvailable(IDMContext, boolean) */ public boolean isTargetAvailable(IDMContext context) { for (IDMContext availableContext : fAvailableContexts) { if (context.equals(availableContext) || DMContexts.isAncestorOf(context, availableContext)) { return true; } } return false; } /** * Clears all the cache data. Equivalent to <code>reset(null)</code>. */ public void reset() { fCachedContexts.clear(); } @Override public void commandRemoved(ICommandToken token) { /* * Do nothing. */ } /* (non-Javadoc) * @see org.eclipse.cdt.dsf.debug.service.command.ICommandListener#commandQueued(org.eclipse.cdt.dsf.debug.service.command.ICommandToken) */ @Override public void commandQueued(ICommandToken token) { /* * Do nothing. */ } /* (non-Javadoc) * @see org.eclipse.cdt.dsf.debug.service.command.ICommandListener#commandDone(org.eclipse.cdt.dsf.debug.service.command.ICommandToken, org.eclipse.cdt.dsf.debug.service.command.ICommandResult) */ @Override public void commandDone(ICommandToken token, ICommandResult result) { /* * We handle the done with a runnable where we initiated the command * so there is nothing to do here. */ } /* * Move the command into our internal sent list. This means we can no longer look at * this command for possible coalescence since it has been given to the debug engine * and is currently being processed. * * @see org.eclipse.cdt.dsf.debug.service.command.ICommandListener#commandSent(org.eclipse.cdt.dsf.debug.service.command.ICommandToken) */ @Override public void commandSent(ICommandToken token) { // Cast the generic ?'s to concrete types in the cache implementation. @SuppressWarnings("unchecked") ICommand<ICommandResult> genericCommand = (ICommand<ICommandResult>)token.getCommand(); CommandInfo cachedCmd = new CommandInfo( CommandStyle.NONCOALESCED, genericCommand, null) ; // It is important to actually fetch the content of the fPendingQCommandsNotYetSent map // instead of only using 'cachedCmd'. This is because although cachedCmd can be considered // equal to unqueuedCommand, it is not identical and we need the full content of unqueuedCommand. // For instance, cachedCmd does not have the list of requestMonitors that unqueuedCommand has. for ( CommandInfo unqueuedCommand : new ArrayList<CommandInfo>(fPendingQCommandsNotYetSent) ) { if ( unqueuedCommand.equals( cachedCmd )) { fPendingQCommandsNotYetSent.remove(unqueuedCommand); fPendingQCommandsSent.add(unqueuedCommand); break; } } } /** * Clears the cache entries for given context. Clears the whole cache if * context parameter is null. */ public void reset(IDMContext dmc) { if (dmc == null) { fCachedContexts.clear(); return; } for (Iterator<IDMContext> itr = fCachedContexts.keySet().iterator(); itr.hasNext();) { IDMContext keyDmc = itr.next(); if (keyDmc != null && (dmc.equals(keyDmc) || DMContexts.isAncestorOf(keyDmc, dmc))) { itr.remove(); } } } }