| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.netbeans.modules.maven.debug; |
| |
| import com.sun.jdi.VMOutOfMemoryException; |
| import java.io.File; |
| import java.io.IOException; |
| import java.net.InetAddress; |
| import java.net.ServerSocket; |
| import java.net.URL; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import javax.swing.Action; |
| import org.apache.maven.artifact.versioning.ArtifactVersion; |
| import org.apache.maven.artifact.versioning.DefaultArtifactVersion; |
| import org.netbeans.api.debugger.DebuggerEngine; |
| import org.netbeans.api.debugger.DebuggerManager; |
| import org.netbeans.api.debugger.jpda.DebuggerStartException; |
| import org.netbeans.api.debugger.jpda.JPDADebugger; |
| import org.netbeans.api.java.classpath.ClassPath; |
| import org.netbeans.api.java.queries.SourceForBinaryQuery; |
| import org.netbeans.api.java.source.BuildArtifactMapper; |
| import org.netbeans.api.project.Project; |
| import org.netbeans.modules.maven.api.Constants; |
| import org.netbeans.modules.maven.api.NbMavenProject; |
| import org.netbeans.modules.maven.api.PluginPropertyUtils; |
| import org.netbeans.modules.maven.api.classpath.ProjectSourcesClassPathProvider; |
| import org.netbeans.modules.maven.api.execute.ExecutionContext; |
| import org.netbeans.modules.maven.api.execute.ExecutionResultChecker; |
| import org.netbeans.modules.maven.api.execute.LateBoundPrerequisitesChecker; |
| import org.netbeans.modules.maven.api.execute.PrerequisitesChecker; |
| import org.netbeans.modules.maven.api.execute.RunConfig; |
| import org.netbeans.modules.maven.api.execute.RunUtils; |
| import org.netbeans.modules.maven.execute.AbstractMavenExecutor; |
| import org.netbeans.modules.maven.execute.OutputTabMaintainer; |
| import org.netbeans.spi.debugger.jpda.EditorContext; |
| import org.netbeans.spi.java.classpath.support.ClassPathSupport; |
| import org.netbeans.spi.project.ActionProvider; |
| import org.netbeans.spi.project.ProjectServiceProvider; |
| import org.openide.LifecycleManager; |
| import org.openide.filesystems.FileObject; |
| import org.openide.loaders.DataObject; |
| import org.openide.util.NbCollections; |
| import org.openide.windows.InputOutput; |
| import org.openide.windows.OutputWriter; |
| |
| /** |
| * |
| * @author mkleint |
| */ |
| @ProjectServiceProvider(service={LateBoundPrerequisitesChecker.class, ExecutionResultChecker.class, PrerequisitesChecker.class}, projectType="org-netbeans-modules-maven") |
| public class DebuggerChecker implements LateBoundPrerequisitesChecker, ExecutionResultChecker, PrerequisitesChecker { |
| private static final String ARGLINE = "argLine"; //NOI18N |
| private static final String MAVENSUREFIREDEBUG = "maven.surefire.debug"; //NOI18N |
| private static final Logger LOGGER = Logger.getLogger(DebuggerChecker.class.getName()); |
| private DebuggerTabMaintainer tabMaintainer; |
| |
| public @Override boolean checkRunConfig(final RunConfig config) { |
| if (config.getProject() == null) { |
| //cannot act on execution without a project instance.. |
| return true; |
| } |
| |
| if ("debug.fix".equals(config.getActionName()) && RunUtils.isCompileOnSaveEnabled(config)) { //NOI18N |
| final String cname = config.getProperties().get("jpda.stopclass"); //NOI18N |
| |
| if (cname != null) { |
| |
| // tstupka: workarounding problems mentioned in issue #242559 |
| // for some reason files do not get saved before "debug.fix" => reload doesn't always catch the changes |
| |
| // also that: |
| // mkleint: this is (was) commented out because one day the apply changed icon was always enabled (even for the case of a non saved modified file). The code is handling that case. |
| // however today I cannot reproduce and the apply changes button is only enabled when the changed file is saved. |
| // |
| |
| Set<DataObject> mods = DataObject.getRegistry().getModifiedSet(); |
| if (!mods.isEmpty()) { |
| //TODO compute is any of the files belong to the source roots of this project. |
| //if so, attach the listener and wait for the notification that the files were compiled |
| ClassPath[] cps = config.getProject().getLookup().lookup(ProjectSourcesClassPathProvider.class).getProjectClassPaths(ClassPath.SOURCE); |
| final Set<URL> urls = new HashSet<URL>(); |
| for (DataObject mod : mods) { |
| for (ClassPath cp : cps) { |
| if (cp.contains(mod.getPrimaryFile())) { |
| FileObject root = cp.findOwnerRoot(mod.getPrimaryFile()); |
| if (root != null) { |
| urls.add(root.toURL()); |
| } |
| } |
| } |
| } |
| LifecycleManager.getDefault().saveAll(); |
| |
| if (!urls.isEmpty()) { |
| final int count = urls.size(); |
| BuildArtifactMapper.ArtifactsUpdated listener = new BuildArtifactMapper.ArtifactsUpdated() { |
| private int countdown = count; |
| @Override |
| public void artifactsUpdated(Iterable<File> artifacts) { |
| //if there are multiple urls we are listening on, wait for the last one. |
| if (countdown > 0) { |
| countdown = countdown - 1; |
| } else { |
| //remove the listeners and then reload |
| for (URL url : urls) { |
| BuildArtifactMapper.removeArtifactsUpdatedListener(url, this); |
| } |
| doReload(config, cname); |
| } |
| } |
| }; |
| for (URL url : urls) { |
| BuildArtifactMapper.addArtifactsUpdatedListener(url, listener); |
| } |
| } else { |
| doReload(config, cname); |
| } |
| |
| //TODO spawn a thread that will interrupt the listening after a timeout just to be sure? |
| //RequestProcessor.getDefault().post(null).cancel(); |
| |
| } else { |
| |
| doReload(config, cname); |
| |
| } |
| return false; |
| } |
| } |
| boolean debug = "true".equalsIgnoreCase(config.getProperties().get(Constants.ACTION_PROPERTY_JPDALISTEN));//NOI18N |
| if (debug && ActionProvider.COMMAND_DEBUG_TEST_SINGLE.equalsIgnoreCase(config.getActionName()) && config.getGoals().contains("surefire:test")) { //NOI18N - just a safeguard |
| String newArgs = config.getProperties().get(MAVENSUREFIREDEBUG); //NOI18N |
| String oldArgs = config.getProperties().get(ARGLINE); //NOI18N |
| |
| String ver = PluginPropertyUtils.getPluginVersion(config.getMavenProject(), Constants.GROUP_APACHE_PLUGINS, Constants.PLUGIN_SUREFIRE); //NOI18N |
| //make sure we have both old surefire-plugin and new surefire-plugin covered |
| // in terms of property definitions. |
| |
| if (ver == null) { |
| ver = "2.4"; //assume 2.4+, will be true for 2.0.9+ where defined explicitly, in older versions it will be the latest version. |
| } |
| ArtifactVersion twopointfour = new DefaultArtifactVersion("2.4"); //NOI18N |
| ArtifactVersion current = new DefaultArtifactVersion(ver); //NOI18N |
| int compare = current.compareTo(twopointfour); |
| if (oldArgs != null && newArgs == null && compare >= 0) { |
| //this case is for custom user mapping in nbactions.xml |
| // we can move it to new property safely IMHO. |
| config.setProperty(MAVENSUREFIREDEBUG, oldArgs); |
| config.setProperty(ARGLINE, null); |
| } |
| if (newArgs != null && compare < 0) { |
| oldArgs = (oldArgs == null ? "" : oldArgs) + " " + newArgs; |
| config.setProperty(ARGLINE, oldArgs); |
| // in older versions this property id dangerous |
| config.setProperty(MAVENSUREFIREDEBUG, null); |
| } |
| } |
| if ("true".equals(config.getProperties().get(Constants.ACTION_PROPERTY_JPDAATTACH))) { // NOI18N |
| try (ServerSocket ss = new ServerSocket()) { |
| ss.bind(null); |
| int port = ss.getLocalPort(); |
| final InetAddress addr = ss.getInetAddress(); |
| String address = addr.isAnyLocalAddress() ? "localhost" : addr.getHostAddress(); |
| config.setProperty(Constants.ACTION_PROPERTY_JPDAATTACH, address + ":" + port); |
| config.setProperty(Constants.ACTION_PROPERTY_JPDAATTACH_ADDRESS, address); |
| config.setProperty(Constants.ACTION_PROPERTY_JPDAATTACH_PORT, "" + port); |
| } catch (IOException ex) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| protected void doReload(final RunConfig config, final String cname) { |
| DebuggerTabMaintainer otm = getOutputTabMaintainer(config.getExecutionName()); |
| |
| InputOutput io = otm.getInputOutput(); |
| io.select(); |
| OutputWriter ow = io.getOut(); |
| try { |
| ow.reset(); |
| } catch (IOException ex) { } |
| |
| try { |
| reload(config.getProject(), ow, cname); |
| } finally { |
| io.getOut().close(); |
| otm.markTab(); |
| } |
| } |
| |
| public @Override boolean checkRunConfig(RunConfig config, ExecutionContext context) { |
| if (config.getProject() == null) { |
| //cannot act on execution without a project instance.. |
| return true; |
| } |
| |
| boolean debug = "true".equalsIgnoreCase(config.getProperties().get(Constants.ACTION_PROPERTY_JPDALISTEN));//NOI18N |
| boolean mavenDebug = "maven".equalsIgnoreCase(config.getProperties().get(Constants.ACTION_PROPERTY_JPDALISTEN)); //NOI18N |
| if (debug || mavenDebug) { |
| String key = "Env.MAVEN_OPTS"; //NOI18N |
| if (mavenDebug) { |
| String vmargs = "-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address}"; //NOI18N |
| String orig = config.getProperties().get(key); |
| if (orig == null) { |
| orig = System.getenv("MAVEN_OPTS"); // NOI18N |
| } |
| config.setProperty(key, orig != null ? orig + ' ' + vmargs : vmargs); |
| } |
| try { |
| JPDAStart start = new JPDAStart(context.getInputOutput(), config.getActionName()); |
| NbMavenProject prj = config.getProject().getLookup().lookup(NbMavenProject.class); |
| start.setName(prj.getMavenProject().getArtifactId()); |
| String stopClass = config.getProperties().get("jpda.stopclass"); |
| if (stopClass == null) { |
| stopClass = (String) config.getInternalProperties().get("jpda.stopclass"); |
| } |
| start.setStopClassName(stopClass); //NOI18N |
| String sm = (String) config.getInternalProperties().get("jpda.stopmethod"); |
| if (sm != null) { |
| start.setStopMethod(sm); |
| } |
| ClassPath addCP = (ClassPath) config.getInternalProperties().get("jpda.additionalClasspath"); |
| if (addCP != null) { |
| start.setAdditionalSourcePath(addCP); |
| } |
| String val = start.execute(config.getProject()); |
| for (Map.Entry<String,String> entry : NbCollections.checkedMapByFilter(config.getProperties(), String.class, String.class, true).entrySet()) { |
| StringBuilder buf = new StringBuilder(entry.getValue()); |
| String replaceItem = "${jpda.address}"; //NOI18N |
| int index = buf.indexOf(replaceItem); |
| while (index > -1) { |
| String newItem = val; |
| newItem = newItem == null ? "" : newItem; //NOI18N |
| buf.replace(index, index + replaceItem.length(), newItem); |
| index = buf.indexOf(replaceItem); |
| } |
| // debug must properly update debug port in runconfig... |
| if(entry.getKey().equals(MAVENSUREFIREDEBUG)) { |
| String address = "address="; //NOI18N |
| index = buf.indexOf(address); |
| if(index > -1) { |
| buf.replace(index + 8, buf.length(), val); |
| } |
| } |
| // System.out.println("setting property=" + key + "=" + buf.toString()); |
| config.setProperty(entry.getKey(), buf.toString()); |
| } |
| config.setProperty("jpda.address", val); //NOI18N |
| } catch (Throwable th) { |
| LOGGER.log(Level.INFO, th.getMessage(), th); |
| } |
| } |
| if (ActionProvider.COMMAND_DEBUG_STEP_INTO.equals(config.getActionName())) { |
| //TODO - change the goal from compile to test-compile in case of file coming from |
| //the test source roots.. |
| } |
| return true; |
| } |
| |
| public @Override void executionResult(RunConfig config, ExecutionContext res, int resultCode) { |
| if (config.getProject() != null && resultCode == 0 && "debug.fix".equals(config.getActionName())) { //NOI18N |
| String cname = config.getProperties().get("jpda.stopclass"); //NOI18N |
| if (cname != null) { |
| reload(config.getProject(), res.getInputOutput().getOut(), cname); |
| } else { |
| res.getInputOutput().getErr().println("Missing jpda.stopclass property in action mapping definition. Cannot reload class."); |
| } |
| } |
| if (resultCode == 0 && config.getProperties().get(Constants.ACTION_PROPERTY_JPDAATTACH_TRIGGER) == null) { |
| try { |
| connect(config); |
| } catch (DebuggerStartException ex) { |
| ex.printStackTrace(res.getInputOutput().getErr()); |
| } |
| } |
| } |
| |
| static void connect(RunConfig config) throws DebuggerStartException { |
| String attachToAddress = config.getProperties().get(Constants.ACTION_PROPERTY_JPDAATTACH); |
| if (attachToAddress != null) { |
| String transport = config.getProperties().get(Constants.ACTION_PROPERTY_JPDAATTACH_TRANSPORT); |
| if (transport == null || "dt_socket".equals(transport)) { |
| int colon = attachToAddress.indexOf(':'); |
| int port; |
| try { |
| port = Integer.parseInt(attachToAddress.substring(colon + 1)); |
| } catch (NumberFormatException ex) { |
| final DebuggerStartException debugEx = new DebuggerStartException("Cannot parse " + attachToAddress.substring(colon + 1) + " as number"); |
| debugEx.initCause(ex); |
| throw debugEx; |
| } |
| String host; |
| if (colon > 0) { |
| host = attachToAddress.substring(0, colon); |
| } else { |
| host = "localhost"; |
| } |
| JPDADebugger.attach(host, port, new Object[0]); |
| } else if ("dt_shmem".equals(transport)) { |
| JPDADebugger.attach(attachToAddress, new Object[0]); |
| } else { |
| LOGGER.log(Level.INFO, "Ignoring unknown transport '"+transport+"'"); |
| } |
| } |
| } |
| |
| |
| public void reload(Project project, OutputWriter logger, String classname) { |
| // check debugger state |
| DebuggerEngine debuggerEngine = DebuggerManager.getDebuggerManager (). |
| getCurrentEngine (); |
| if (debuggerEngine == null) { |
| logger.println("No debugging sessions was found."); |
| return; |
| } |
| JPDADebugger debugger = debuggerEngine.lookupFirst |
| (null, JPDADebugger.class); |
| if (debugger == null) { |
| logger.println("Current debugger is not JPDA one."); |
| return; |
| } |
| if (!debugger.canFixClasses ()) { |
| logger.println("The debugger does not support Fix action."); |
| return; |
| } |
| if (debugger.getState () == JPDADebugger.STATE_DISCONNECTED) { |
| logger.println("The debugger is not running"); |
| return; |
| } |
| |
| Map<String, byte[]> map = new HashMap<String, byte[]>(); |
| EditorContext editorContext = DebuggerManager. |
| getDebuggerManager ().lookupFirst (null, EditorContext.class); |
| |
| String clazz = classname.replace('.', '/') + ".class"; //NOI18N |
| ProjectSourcesClassPathProvider prv = project.getLookup().lookup(ProjectSourcesClassPathProvider.class); |
| ClassPath[] ccp = prv.getProjectClassPaths(ClassPath.COMPILE); |
| FileObject fo2 = null; |
| for (ClassPath cp : ccp) { |
| fo2 = cp.findResource(clazz); |
| if (fo2 != null) { |
| break; |
| } |
| } |
| if (fo2 != null) { |
| try { |
| String basename = fo2.getName(); |
| for (FileObject classfile : fo2.getParent().getChildren()) { |
| String basename2 = classfile.getName(); |
| if (/*#220338*/!"class".equals(classfile.getExt()) || (!basename2.equals(basename) && !basename2.startsWith(basename + '$'))) { |
| continue; |
| } |
| String url = classToSourceURL(classfile, logger); |
| if (url != null) { |
| editorContext.updateTimeStamp(debugger, url); |
| } |
| map.put(classname + basename2.substring(basename.length()), classfile.asBytes()); |
| } |
| } catch (IOException ex) { |
| ex.printStackTrace (); |
| } |
| } |
| |
| if (map.isEmpty()) { |
| logger.println("No class to reload"); |
| return; |
| } else { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("Classes to reload:\n"); |
| for (String c : map.keySet()) { |
| sb.append(" "); |
| sb.append(c); |
| sb.append("\n"); |
| } |
| logger.println(sb); |
| } |
| String error = null; |
| try { |
| debugger.fixClasses (map); |
| } catch (UnsupportedOperationException uoex) { |
| error = "The virtual machine does not support this operation: "+uoex.getLocalizedMessage(); |
| } catch (NoClassDefFoundError ncdfex) { |
| error = "The bytes don't correspond to the class type (the names don't match): "+ncdfex.getLocalizedMessage(); |
| } catch (VerifyError ver) { |
| error = "A \"verifier\" detects that a class, though well formed, contains an internal inconsistency or security problem: "+ver.getLocalizedMessage(); |
| } catch (UnsupportedClassVersionError ucver) { |
| error = "The major and minor version numbers in bytes are not supported by the VM. "+ucver.getLocalizedMessage(); |
| } catch (ClassFormatError cfer) { |
| error = "The bytes do not represent a valid class. "+cfer.getLocalizedMessage(); |
| LOGGER.log(Level.INFO, error, cfer); //#216376 |
| } catch (ClassCircularityError ccer) { |
| error = "A circularity has been detected while initializing a class: "+ccer.getLocalizedMessage(); |
| } catch (VMOutOfMemoryException oomex) { |
| error = "Out of memory in the target VM has occurred during class reload."; |
| } |
| if (error != null) { |
| logger.println(error); |
| } else { |
| logger.println("Code updated"); |
| } |
| } |
| |
| private String classToSourceURL (FileObject fo, OutputWriter logger) { |
| ClassPath cp = ClassPath.getClassPath (fo, ClassPath.EXECUTE); |
| if (cp == null) { |
| return null; |
| } |
| FileObject root = cp.findOwnerRoot (fo); |
| String resourceName = cp.getResourceName (fo, '/', false); |
| if (resourceName == null) { |
| logger.println("Can not find classpath resource for "+fo+", skipping..."); |
| return null; |
| } |
| int i = resourceName.indexOf ('$'); |
| if (i > 0) { |
| resourceName = resourceName.substring (0, i); |
| } |
| FileObject[] sRoots = SourceForBinaryQuery.findSourceRoots |
| (root.toURL ()).getRoots (); |
| ClassPath sourcePath = ClassPathSupport.createClassPath (sRoots); |
| FileObject rfo = sourcePath.findResource (resourceName + ".java"); |
| if (rfo == null) { |
| return null; |
| } |
| return rfo.toURL ().toExternalForm (); |
| } |
| |
| private synchronized DebuggerTabMaintainer getOutputTabMaintainer(String name) { |
| if(tabMaintainer == null) { |
| tabMaintainer = new DebuggerTabMaintainer(name); |
| } |
| return tabMaintainer; |
| } |
| |
| private static class TabCtx { |
| AbstractMavenExecutor.OptionsAction options; |
| @Override |
| protected TabCtx clone() { |
| TabCtx c = new TabCtx(); |
| c.options = options; |
| return c; |
| } |
| } |
| |
| private static class DebuggerTabMaintainer extends OutputTabMaintainer<TabCtx> { |
| private TabCtx tabContext = new TabCtx(); |
| public DebuggerTabMaintainer(String name) { |
| super(name); |
| } |
| @Override |
| protected Class<TabCtx> tabContextType() { |
| return TabCtx.class; |
| } |
| @Override |
| protected TabCtx createContext() { |
| return tabContext.clone(); |
| } |
| @Override protected void reassignAdditionalContext(TabCtx tabContext) { |
| this.tabContext = tabContext; |
| } |
| @Override |
| protected Action[] createNewTabActions() { |
| tabContext.options = new AbstractMavenExecutor.OptionsAction(); |
| return new Action[] { tabContext.options }; |
| } |
| void markTab() { |
| markFreeTab(); |
| } |
| } |
| |
| } |