/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.debug;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.wicket.RequestCycle;
import org.eclipse.dltk.rhino.dbgp.DBGPDebugger;
import org.eclipse.dltk.rhino.dbgp.DBGPDebugger.ITerminationListener;
import org.eclipse.dltk.rhino.dbgp.DBGPStackManager;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.RhinoException;
import org.mozilla.javascript.Scriptable;
import com.servoy.j2db.ClientState;
import com.servoy.j2db.IApplication;
import com.servoy.j2db.IDebugClient;
import com.servoy.j2db.IServiceProvider;
import com.servoy.j2db.IWebClientApplication;
import com.servoy.j2db.J2DBGlobals;
import com.servoy.j2db.persistence.AbstractBase;
import com.servoy.j2db.persistence.IScriptProvider;
import com.servoy.j2db.persistence.ScriptCalculation;
import com.servoy.j2db.scripting.LazyCompilationScope;
import com.servoy.j2db.scripting.ScriptEngine;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.ExtendableURLClassLoader;
/**
* @author jcompagner
*
*/
public class RemoteDebugScriptEngine extends ScriptEngine implements ITerminationListener
{
private static Socket socket;
private static volatile ServoyDebugger debugger;
private static volatile ServerSocket ss;
private static final ConcurrentHashMap<IApplication, List<Context>> contexts = new ConcurrentHashMap<IApplication, List<Context>>();
private static final ConnectionTester connectionTester = new ConnectionTester();
private static final ContextFactory.Listener contextListener = new ContextFactory.Listener()
{
private final ThreadLocal<DBGPStackManager> manager = new ThreadLocal<DBGPStackManager>();
public void contextReleased(Context cx)
{
IServiceProvider sp = J2DBGlobals.getServiceProvider();
if (sp instanceof IApplication && sp instanceof IDebugClient)
{
IApplication application = (IApplication)sp;
if (manager.get() != null && application.isEventDispatchThread() && !(Thread.currentThread() instanceof ServoyDebugger))
{
if (debugger != null) debugger.setStackManager(manager.get());
manager.remove();
}
List<Context> list = contexts.get(application);
if (list != null) list.remove(cx);
}
}
public void contextCreated(Context cx)
{
IServiceProvider sp = J2DBGlobals.getServiceProvider();
if (sp instanceof IApplication && sp instanceof IDebugClient)
{
IApplication application = (IApplication)sp;
if (debugger != null && debugger.isInited)
{
// executing can be done multiply in a thread (calc)
// only allow the event threads (AWT and web client request thread) to debug.
boolean isDispatchThread = application.isEventDispatchThread() && !(Thread.currentThread() instanceof ServoyDebugger);
if (isDispatchThread && application instanceof IWebClientApplication)
{
isDispatchThread = RequestCycle.get() != null; // for web client test extra if this is a Request thread.
}
if (isDispatchThread)
{
cx.setApplicationClassLoader(application.getBeanManager().getClassLoader());
manager.set(debugger.getStackManager());
debugger.setContext(cx);
cx.setDebugger(debugger, null);
cx.setGeneratingDebug(true);
cx.setOptimizationLevel(-1);
}
else if (!(cx.getApplicationClassLoader() instanceof ExtendableURLClassLoader))
{
cx.setApplicationClassLoader(application.getBeanManager().getClassLoader());
}
}
else
{
manager.remove();
}
// context for this client
List<Context> list = contexts.get(application);
if (list == null)
{
list = Collections.synchronizedList(new ArrayList<Context>());
contexts.put(application, list);
}
list.add(cx);
}
}
};
static
{
ContextFactory.getGlobal().addListener(contextListener);
}
private static final List<IProfileListener> profileListeners = new ArrayList<IProfileListener>();
public static int startupDebugger()
{
if (ss != null) return ss.getLocalPort();
try
{
ss = new ServerSocket(0);
}
catch (IOException e1)
{
Debug.error(e1);
return -1;
}
Runnable debugThread = new Runnable()
{
public void run()
{
while (ss != null)
{
try
{
socket = ss.accept();
socket.setKeepAlive(true);
if (Debug.tracing())
{
Debug.trace("Socket " + socket + " Accepted, staring connect thread");
}
new Thread(new Runnable()
{
public void run()
{
try
{
connect("globals.js", "remote:" + ss.getLocalPort());
}
catch (Exception e)
{
Debug.error("Error connectiong to a debug", e);
}
}
}, "ScriptDebug Connector").start();
}
catch (Exception e)
{
Debug.error("Error accepting debug connections", e);
try
{
ss.close();
}
catch (IOException e1)
{
}
ss = null;
}
}
}
};
new Thread(debugThread, "Script Debug accept thread").start();
return ss.getLocalPort();
}
private boolean listenerAdded;
private final AtomicInteger executingFunction = new AtomicInteger(0);
/**
* @param app
*/
public RemoteDebugScriptEngine(IApplication app)
{
super(app);
}
public static boolean isConnected()
{
return isConnected(5);
}
public static boolean isConnected(int maxWaits)
{
int i = 0;
while (ss == null && i++ < maxWaits)
{
if (Debug.tracing())
{
Debug.trace("Waiting for Server socket " + i + " of " + maxWaits + " tries");
}
try
{
Thread.sleep(1000); // wait for a bit until socket is there.
}
catch (InterruptedException e)
{
// ignore
}
}
if (ss == null) return false;
i = 0;
while (debugger == null && i++ < 10 * maxWaits)
{
// just wait a few seconds if a debugger has to be created.
if (Debug.tracing())
{
Debug.trace("Waiting for debugger to be created" + i + " of 50 tries");
}
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
// ignore
}
}
int counter = 0;
while (debugger != null && socket != null && !debugger.isInited && socket.isConnected() && !socket.isClosed() && socket.isBound())
{
if (counter++ > 5)
{
Debug.trace("Debug Socket still not connected after 5 tries, " + socket);
return false;
}
synchronized (debugger)
{
try
{
debugger.wait(1000);
}
catch (InterruptedException e)
{
Debug.error(e);
}
}
}
if (debugger != null && socket != null && socket.isConnected() && !socket.isClosed() && socket.isBound() && debugger.isInited)
{
return connectionTester.checkState();
}
return false;
}
/**
* @see com.servoy.j2db.scripting.ScriptEngine#compileFunction(com.servoy.j2db.persistence.IScriptProvider, org.mozilla.javascript.Scriptable)
*/
@Override
public Function compileFunction(IScriptProvider sp, Scriptable scope) throws Exception
{
String sourceName = sp.getDataProviderID();
AbstractBase base = (AbstractBase)sp;
String filename = base.getSerializableRuntimeProperty(IScriptProvider.FILENAME);
if (filename != null)
{
sourceName = filename;
}
Context cx = Context.enter();
try
{
cx.setGeneratingDebug(true);
cx.setOptimizationLevel(-1);
return compileScriptProvider(sp, scope, cx, sourceName);
}
catch (RhinoException ee)
{
application.reportJSError("Compilation failed for method: " + sp.getDataProviderID() + ", " + ee.getMessage(), ee);
throw ee;
}
catch (Exception e)
{
Debug.error("Compilation failed for method: " + sp.getDataProviderID());
throw e;
}
finally
{
Context.exit();
}
}
public boolean recompileScriptCalculation(ScriptCalculation sc)
{
try
{
Scriptable tableScope = getExistingTableScrope(sc.getTable());
if (tableScope instanceof LazyCompilationScope)
{
try
{
((LazyCompilationScope)tableScope).put(sc, sc);
return true;
}
catch (Exception ex)
{
application.reportJSError("compile failed: " + sc.getDataProviderID(), ex);
}
}
}
catch (Exception e)
{
Debug.error(e);
}
return false;
}
/**
* @param file
* @param sessionId
* @return
* @throws IOException
*/
private static boolean connect(String file, String sessionId) throws IOException
{
Context cx = Context.enter();
try
{
debugger = new ServoyDebugger(socket, file, sessionId, cx, profileListeners);
if (Debug.tracing())
{
Debug.trace("Created Servoy Debugger on socket " + socket + ", starting the debugger command thread.");
}
debugger.start();
cx.setDebugger(debugger, null);
cx.setGeneratingDebug(true);
cx.setOptimizationLevel(-1);
}
finally
{
Context.exit();
}
if (!socket.isConnected())
{
socket = null;
debugger = null;
return false;
}
return true;
}
@Override
public Object executeFunction(Function f, Scriptable scope, Scriptable thisObject, Object[] args, boolean focusEvent, boolean throwException)
throws Exception
{
if (debugger != null && !listenerAdded)
{
listenerAdded = true;
debugger.addTerminationListener(RemoteDebugScriptEngine.this);
}
try
{
executingFunction.incrementAndGet();
return super.executeFunction(f, scope, thisObject, args, focusEvent, throwException);
}
finally
{
executingFunction.decrementAndGet();
}
}
/**
*
*/
public DBGPDebugger getDebugger()
{
if (debugger != null && isConnected())
{
return debugger;
}
return null;
}
/**
* @see com.servoy.j2db.scripting.ScriptEngine#destroy()
*/
@Override
public void destroy()
{
if (debugger != null)
{
List<Context> list = contexts.remove(application);
if (list != null && list.size() > 0)
{
Context[] array;
synchronized (list)
{
array = list.toArray(new Context[list.size()]);
list.clear();
}
for (Context cx : array)
{
DBGPStackManager manager = DBGPStackManager.removeManager(cx);
if (manager != null)
{
manager.stop();
}
}
}
debugger.removeTerminationListener(this);
}
super.destroy();
}
/**
* @see org.eclipse.dltk.rhino.dbgp.DBGPDebugger.ITerminationListener#debuggerTerminated()
*/
public void debuggerTerminated()
{
socket = null;
debugger = null;
((ClientState)application).invokeLater(new Runnable()
{
public void run()
{
((ClientState)application).shutDown(true);
}
});
}
@Override
public boolean isAWTSuspendedRunningScript()
{
// actually this returns true if the AWT thread is suspended in debugger (not any thread)
if (executingFunction.get() > 0 && debugger != null)
{
DBGPStackManager sm = debugger.getStackManager();
return sm != null && sm.isSuspended();
}
return false;
}
/**
* @param profileListener
*/
public static void registerProfileListener(IProfileListener profileListener)
{
profileListeners.add(profileListener);
}
public static void deregisterProfileListener(IProfileListener profileListener)
{
profileListeners.remove(profileListener);
}
private static final class ConnectionTester implements Runnable
{
private volatile boolean executing;
private volatile long startTime;
private volatile boolean connected = true;
private Thread t;
@Override
public void run()
{
try
{
debugger.outputStdOut("");
connected = isSocketValid();
if (!connected && socket != null)
{
debugger = null;
socket = null;
}
}
finally
{
synchronized (this)
{
executing = false;
}
}
}
private boolean isSocketValid()
{
return socket != null && !socket.isClosed() && socket.isConnected() && socket.isBound();
}
public synchronized boolean checkState()
{
if (debugger == null) return false;
if (!executing)
{
if (isSocketValid())
{
executing = true;
startTime = System.currentTimeMillis();
t = new Thread(this);
t.start();
}
}
else if ((System.currentTimeMillis() - startTime) > 2000)
{
// if it was already executing and 2 seconds are passed, checking the connection, close the socket.
try
{
socket.close();
}
catch (IOException e)
{
}
connected = false;
}
return connected;
}
public synchronized void waitABitIfNeededAndThenRun(int maxMSToWait, Runnable toRun)
{
if (executing)
{
try
{
t.join(maxMSToWait);
// TODO we could even try to interrupt it if it takes too long...
}
catch (InterruptedException e)
{
// just continue and execute anyway
}
}
toRun.run();
}
}
public static void stopExecutingCurrentFunction()
{
if (debugger != null)
{
connectionTester.waitABitIfNeededAndThenRun(1000, new Runnable()
{
@Override
public void run()
{
if (debugger != null)
{
try
{
Context.enter();
debugger.sendEnd(true);
}
catch (Throwable t)
{
t.printStackTrace();
}
finally
{
Context.exit();
}
}
}
});
}
}
}