/*******************************************************************************
* Copyright (c) 2011, 2016 Mentor Graphics 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:
* Mentor Graphics - Initial API and implementation
* Jason Litton (Sage Electronic Engineering, LLC) - Use Dynamic Tracing option (Bug 379169)
*******************************************************************************/
package org.eclipse.cdt.dsf.gdb.service.command;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.eclipse.cdt.dsf.debug.service.command.ICommand;
import org.eclipse.cdt.dsf.debug.service.command.ICommandControl;
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.gdb.IGdbDebugPreferenceConstants;
import org.eclipse.cdt.dsf.gdb.internal.GdbDebugOptions;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.cdt.dsf.mi.service.command.AbstractMIControl;
import org.eclipse.cdt.dsf.mi.service.command.commands.MICommand;
import org.eclipse.cdt.dsf.mi.service.command.output.MIInfo;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.core.runtime.preferences.InstanceScope;
/**
* The command timeout manager registers itself as a command listener and monitors
* the command execution time. The goal of this implementation is to gracefully
* handle disruptions in the communication between Eclipse and GDB.
*
* The algorithm used by this class is based on the assumption that the command
* execution in GDB is sequential even though DSF can send up to 3 commands at
* a time to GDB (see {@link AbstractMIControl}).
*
* @since 4.1
*/
public class GdbCommandTimeoutManager implements ICommandListener, IPreferenceChangeListener {
public interface ICommandTimeoutListener {
void commandTimedOut( ICommandToken token );
}
/**
* @deprecated The DEBUG flag is replaced with the GdbDebugOptions.DEBUG_COMMAND_TIMEOUTS
*/
@Deprecated
public final static boolean DEBUG = Boolean.parseBoolean( Platform.getDebugOption( "org.eclipse.cdt.dsf.gdb/debug/timeouts" ) ); //$NON-NLS-1$
private class QueueEntry {
private long fTimestamp;
private ICommandToken fCommandToken;
private QueueEntry( long timestamp, ICommandToken commandToken ) {
super();
fTimestamp = timestamp;
fCommandToken = commandToken;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals( Object obj ) {
if ( obj instanceof QueueEntry ) {
return fCommandToken.equals( ((QueueEntry)obj).fCommandToken );
}
return false;
}
}
private enum TimerThreadState {
INITIALIZING,
RUNNING,
HALTED,
SHUTDOWN
}
private class TimerThread extends Thread {
private BlockingQueue<QueueEntry> fQueue;
private int fWaitTimeout = IGdbDebugPreferenceConstants.COMMAND_TIMEOUT_VALUE_DEFAULT;
private TimerThreadState fState = TimerThreadState.INITIALIZING;
TimerThread( BlockingQueue<QueueEntry> queue, int timeout ) {
super();
setName( "GDB Command Timer Thread" ); //$NON-NLS-1$
fQueue = queue;
setWaitTimout( timeout );
}
/* (non-Javadoc)
* @see java.lang.Thread#run()
*/
@Override
public void run() {
setTimerThreadState( ( getWaitTimeout() > 0 ) ?
TimerThreadState.RUNNING : TimerThreadState.HALTED );
doRun();
}
private void doRun() {
while ( getTimerThreadState() != TimerThreadState.SHUTDOWN ) {
if ( getTimerThreadState() == TimerThreadState.HALTED ) {
halted();
}
else {
running();
}
}
}
private void halted() {
fQueue.clear();
try {
synchronized( TimerThread.this ) {
wait();
}
}
catch( InterruptedException e ) {
}
}
private void running() {
try {
while( getTimerThreadState() == TimerThreadState.RUNNING ) {
// Use the minimum of all timeout values > 0 as the wait timeout.
long timeout = getWaitTimeout();
QueueEntry entry = fQueue.peek();
if ( entry != null ) {
// Calculate the time elapsed since the execution of this command started
// and compare it with the command's timeout value.
// If the elapsed time is greater or equal than the timeout value the command
// is marked as timed out. Otherwise, schedule the next check when the timeout
// expires.
long commandTimeout = getTimeoutForCommand( entry.fCommandToken.getCommand() );
if ( GdbDebugOptions.DEBUG_COMMAND_TIMEOUTS ) {
String commandText = entry.fCommandToken.getCommand().toString();
if ( commandText.endsWith( "\n" ) ) //$NON-NLS-1$
commandText = commandText.substring( 0, commandText.length() - 1 );
printDebugMessage( String.format( "Processing command '%s', command timeout is %d", //$NON-NLS-1$
commandText, Long.valueOf( commandTimeout ) ) );
}
long currentTime = System.currentTimeMillis();
long elapsedTime = currentTime - entry.fTimestamp;
if ( commandTimeout <= elapsedTime ) {
processTimedOutCommand( entry.fCommandToken );
fQueue.remove( entry );
// Reset the timestamp of the next command in the queue because
// regardless how long the command has been in the queue GDB will
// start executing it only when the execution of the previous command
// is completed.
QueueEntry nextEntry = fQueue.peek();
if ( nextEntry != null ) {
setTimeStamp( currentTime, nextEntry );
}
}
else {
// Adjust the wait timeout because the time remaining for
// the current command to expire may be less than the current wait timeout.
timeout = Math.min( timeout, commandTimeout - elapsedTime );
if ( GdbDebugOptions.DEBUG_COMMAND_TIMEOUTS ) {
String commandText = entry.fCommandToken.getCommand().toString();
if ( commandText.endsWith( "\n" ) ) //$NON-NLS-1$
commandText = commandText.substring( 0, commandText.length() - 1 );
printDebugMessage( String.format( "Setting timeout %d for command '%s'", Long.valueOf( timeout ), commandText ) ); //$NON-NLS-1$
}
}
}
synchronized( TimerThread.this ) {
wait( timeout );
}
}
}
catch( InterruptedException e ) {
}
}
private void shutdown() {
setTimerThreadState( TimerThreadState.SHUTDOWN );
}
private synchronized void setWaitTimout( int waitTimeout ) {
fWaitTimeout = waitTimeout;
if ( GdbDebugOptions.DEBUG_COMMAND_TIMEOUTS )
printDebugMessage( String.format( "Wait timeout is set to %d", Integer.valueOf( fWaitTimeout ) ) ); //$NON-NLS-1$
}
private synchronized int getWaitTimeout() {
return fWaitTimeout;
}
private synchronized void setTimerThreadState( TimerThreadState state ) {
fState = state;
interrupt();
}
private synchronized TimerThreadState getTimerThreadState() {
return fState;
}
}
private static final String TIMEOUT_TRACE_IDENTIFIER = "[TMO]"; //$NON-NLS-1$
private ICommandControl fCommandControl;
private boolean fTimeoutEnabled = false;
private int fTimeout = 0;
private TimerThread fTimerThread;
private BlockingQueue<QueueEntry> fCommandQueue = new LinkedBlockingQueue<QueueEntry>();
private CustomTimeoutsMap fCustomTimeouts = new CustomTimeoutsMap();
private ListenerList fListeners;
public GdbCommandTimeoutManager( ICommandControl commandControl ) {
fCommandControl = commandControl;
fListeners = new ListenerList();
}
public void initialize() {
IEclipsePreferences node = InstanceScope.INSTANCE.getNode( GdbPlugin.PLUGIN_ID );
fTimeoutEnabled = Platform.getPreferencesService().getBoolean(
GdbPlugin.PLUGIN_ID,
IGdbDebugPreferenceConstants.PREF_COMMAND_TIMEOUT,
false,
null );
fTimeout = Platform.getPreferencesService().getInt(
GdbPlugin.PLUGIN_ID,
IGdbDebugPreferenceConstants.PREF_COMMAND_TIMEOUT_VALUE,
IGdbDebugPreferenceConstants.COMMAND_TIMEOUT_VALUE_DEFAULT,
null );
fCustomTimeouts.initializeFromMemento( Platform.getPreferencesService().getString(
GdbPlugin.PLUGIN_ID,
IGdbDebugPreferenceConstants.PREF_COMMAND_CUSTOM_TIMEOUTS,
"", //$NON-NLS-1$
null ) );
node.addPreferenceChangeListener( this );
fCommandControl.addCommandListener( this );
fTimerThread = new TimerThread( fCommandQueue, calculateWaitTimeout() );
fTimerThread.start();
}
public void dispose() {
fTimerThread.shutdown();
fListeners.clear();
InstanceScope.INSTANCE.getNode( GdbPlugin.PLUGIN_ID ).removePreferenceChangeListener( this );
fCommandControl.removeCommandListener( this );
fCommandQueue.clear();
fCustomTimeouts.clear();
}
/* (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 ) {
}
/* (non-Javadoc)
* @see org.eclipse.cdt.dsf.debug.service.command.ICommandListener#commandSent(org.eclipse.cdt.dsf.debug.service.command.ICommandToken)
*/
@Override
public void commandSent( ICommandToken token ) {
if ( !isTimeoutEnabled() )
return;
int commandTimeout = getTimeoutForCommand( token.getCommand() );
if ( GdbDebugOptions.DEBUG_COMMAND_TIMEOUTS ) {
String commandText = token.getCommand().toString();
if ( commandText.endsWith( "\n" ) ) //$NON-NLS-1$
commandText = commandText.substring( 0, commandText.length() - 1 );
printDebugMessage( String.format( "Command '%s' sent, timeout = %d", commandText, Integer.valueOf( commandTimeout ) ) ); //$NON-NLS-1$
}
if ( commandTimeout == 0 )
// Skip commands with no timeout
return;
try {
fCommandQueue.put( new QueueEntry( System.currentTimeMillis(), token ) );
}
catch( InterruptedException e ) {
// ignore
}
}
/* (non-Javadoc)
* @see org.eclipse.cdt.dsf.debug.service.command.ICommandListener#commandRemoved(org.eclipse.cdt.dsf.debug.service.command.ICommandToken)
*/
@Override
public void commandRemoved( ICommandToken token ) {
}
/* (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 ) {
if ( !isTimeoutEnabled() )
return;
fCommandQueue.remove( new QueueEntry( 0, token ) );
if ( GdbDebugOptions.DEBUG_COMMAND_TIMEOUTS ) {
String commandText = token.getCommand().toString();
if ( commandText.endsWith( "\n" ) ) //$NON-NLS-1$
commandText = commandText.substring( 0, commandText.length() - 1 );
printDebugMessage( String.format( "Command '%s' is done", commandText ) ); //$NON-NLS-1$
}
// Reset the timestamp of the next command in the queue because
// regardless how long it has been in the queue GDB will start
// executing it only when the execution of the previous command
// is completed.
QueueEntry nextEntry = fCommandQueue.peek();
if ( nextEntry != null ) {
setTimeStamp( System.currentTimeMillis(), nextEntry );
}
}
/* (non-Javadoc)
* @see org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener#preferenceChange(org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent)
*/
@Override
public void preferenceChange( PreferenceChangeEvent event ) {
String property = event.getKey();
if ( IGdbDebugPreferenceConstants.PREF_COMMAND_TIMEOUT.equals( property ) ) {
// The new value is null when the timeout support is disabled.
if ( event.getNewValue() == null || !event.getNewValue().equals( event.getOldValue() ) ) {
fTimeoutEnabled = ( event.getNewValue() != null ) ?
Boolean.parseBoolean( event.getNewValue().toString() ) : Boolean.FALSE;
updateWaitTimeout();
fTimerThread.setTimerThreadState( ( fTimerThread.getWaitTimeout() > 0 ) ?
TimerThreadState.RUNNING : TimerThreadState.HALTED );
}
}
else if ( IGdbDebugPreferenceConstants.PREF_COMMAND_TIMEOUT_VALUE.equals( property ) ) {
if ( event.getNewValue() == null || !event.getNewValue().equals( event.getOldValue() ) ) {
try {
fTimeout = ( event.getNewValue() != null ) ?
Integer.parseInt( event.getNewValue().toString() ) :
IGdbDebugPreferenceConstants.COMMAND_TIMEOUT_VALUE_DEFAULT;
updateWaitTimeout();
fTimerThread.setTimerThreadState( ( fTimerThread.getWaitTimeout() > 0 ) ?
TimerThreadState.RUNNING : TimerThreadState.HALTED );
}
catch( NumberFormatException e ) {
GdbPlugin.getDefault().getLog().log( new Status( IStatus.ERROR, GdbPlugin.PLUGIN_ID, "Invalid timeout value" ) ); //$NON-NLS-1$
}
}
}
else if ( IGdbDebugPreferenceConstants.PREF_COMMAND_CUSTOM_TIMEOUTS.equals( property ) ) {
if ( event.getNewValue() instanceof String ) {
fCustomTimeouts.initializeFromMemento( (String)event.getNewValue() );
}
else if ( event.getNewValue() == null ) {
fCustomTimeouts.clear();
}
updateWaitTimeout();
fTimerThread.setTimerThreadState( ( fTimerThread.getWaitTimeout() > 0 ) ?
TimerThreadState.RUNNING : TimerThreadState.HALTED );
}
}
protected int getTimeoutForCommand( ICommand<? extends ICommandResult> command ) {
if ( !(command instanceof MICommand<?>) )
return 0;
@SuppressWarnings( "unchecked" )
Integer timeout = fCustomTimeouts.get( ((MICommand<? extends MIInfo>)command).getOperation() );
return ( timeout != null ) ? timeout.intValue() : fTimeout;
}
protected void processTimedOutCommand( ICommandToken token ) {
if ( GdbDebugOptions.DEBUG_COMMAND_TIMEOUTS ) {
String commandText = token.getCommand().toString();
if ( commandText.endsWith( "\n" ) ) //$NON-NLS-1$
commandText = commandText.substring( 0, commandText.length() - 1 );
printDebugMessage( String.format( "Command '%s' is timed out", commandText ) ); //$NON-NLS-1$
}
for ( Object l : fListeners.getListeners() ) {
((ICommandTimeoutListener)l).commandTimedOut( token );
}
}
public void addCommandTimeoutListener( ICommandTimeoutListener listener ) {
fListeners.add( listener );
}
public void removeCommandTimeoutListener( ICommandTimeoutListener listener ) {
fListeners.remove( listener );
}
private void updateWaitTimeout() {
fTimerThread.setWaitTimout( calculateWaitTimeout() );
}
private boolean isTimeoutEnabled() {
return fTimeoutEnabled;
}
private void printDebugMessage( String message ) {
if(GdbDebugOptions.DEBUG_COMMAND_TIMEOUTS) {
GdbDebugOptions.trace(String.format( "%s %s %s\n", GdbPlugin.getDebugTime(), TIMEOUT_TRACE_IDENTIFIER, message)); //$NON-NLS-1$
}
}
private int calculateWaitTimeout() {
int waitTimeout = 0;
if ( isTimeoutEnabled() ) {
waitTimeout = fTimeout;
int minCustomTimeout = Integer.MAX_VALUE;
for ( Integer t : fCustomTimeouts.values() ) {
if ( t.intValue() > 0 ) {
minCustomTimeout = Math.min( minCustomTimeout, t.intValue() );
}
}
if ( minCustomTimeout > 0 ) {
waitTimeout = ( waitTimeout == 0 ) ?
minCustomTimeout : Math.min( waitTimeout, minCustomTimeout );
}
}
return waitTimeout;
}
private void setTimeStamp( long currentTime, QueueEntry nextEntry ) {
if ( nextEntry != null ) {
nextEntry.fTimestamp = currentTime;
if ( GdbDebugOptions.DEBUG_COMMAND_TIMEOUTS ) {
String commandText = nextEntry.fCommandToken.getCommand().toString();
if ( commandText.endsWith( "\n" ) ) //$NON-NLS-1$
commandText = commandText.substring( 0, commandText.length() - 1 );
printDebugMessage( String.format( "Setting the timestamp for command '%s' to %d", commandText, Long.valueOf( currentTime ) ) ); //$NON-NLS-1$
}
}
}
}