| /* |
| * 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.javascript.v8debug.sources; |
| |
| import java.beans.PropertyChangeListener; |
| import java.beans.PropertyChangeSupport; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.Phaser; |
| import java.util.concurrent.Semaphore; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import org.netbeans.api.debugger.ActionsManager; |
| import org.netbeans.api.debugger.LazyActionsManagerListener; |
| import org.netbeans.lib.v8debug.PropertyBoolean; |
| import org.netbeans.lib.v8debug.V8Arguments; |
| import org.netbeans.lib.v8debug.V8Command; |
| import org.netbeans.lib.v8debug.V8Request; |
| import org.netbeans.lib.v8debug.V8Response; |
| import org.netbeans.lib.v8debug.V8Script; |
| import org.netbeans.lib.v8debug.V8StepAction; |
| import org.netbeans.lib.v8debug.commands.ChangeLive; |
| import org.netbeans.lib.v8debug.commands.ChangeLive.ChangeLog.BreakpointUpdate; |
| import org.netbeans.lib.v8debug.commands.ChangeLive.ChangeLog.BreakpointUpdate.Position; |
| import org.netbeans.lib.v8debug.commands.Continue; |
| import org.netbeans.lib.v8debug.commands.Source; |
| import org.netbeans.modules.javascript.v8debug.ScriptsHandler; |
| import org.netbeans.modules.javascript.v8debug.V8Debugger; |
| import org.netbeans.modules.javascript.v8debug.V8DebuggerEngineProvider; |
| import org.netbeans.modules.javascript.v8debug.api.DebuggerOptions; |
| import org.netbeans.modules.javascript.v8debug.frames.CallFrame; |
| import org.netbeans.spi.debugger.ActionsProvider; |
| import org.netbeans.spi.debugger.ContextProvider; |
| import org.openide.filesystems.FileAttributeEvent; |
| import org.openide.filesystems.FileChangeListener; |
| import org.openide.filesystems.FileEvent; |
| import org.openide.filesystems.FileObject; |
| import org.openide.filesystems.FileRenameEvent; |
| import org.openide.filesystems.FileUtil; |
| import org.openide.util.Exceptions; |
| import org.openide.util.RequestProcessor; |
| |
| /** |
| * Support for live application of saved files to the V8. |
| * |
| * @author Martin Entlicher |
| */ |
| public final class ChangeLiveSupport { |
| |
| private static final Logger LOG = Logger.getLogger(ChangeLiveSupport.class.getName()); |
| |
| private static final String PREP_REGEX = "^(\\(function.*\\(.*\\).*\\{ ).*"; |
| private static final Pattern PREP_PATTERN = Pattern.compile(PREP_REGEX, Pattern.MULTILINE | Pattern.DOTALL); |
| |
| //private static final String PREP_TEXT = "(function (exports, require, module, __filename, __dirname) { "; |
| private static final String APP_TEXT = "})();"; |
| |
| public static final String PROP_CHANGES = "changes"; |
| |
| private final V8Debugger dbg; |
| private final FileChangeListener sourceChangeListener; |
| private FileChangeDelivery fileChangeDelivery = new FileChangeDelivery(); |
| private final File[] sourceChangeRoots; |
| private final RequestProcessor rp = new RequestProcessor(ChangeLiveSupport.class); |
| private final PropertyChangeSupport pcl = new PropertyChangeSupport(this); |
| private volatile boolean haveChanges = false; |
| |
| public ChangeLiveSupport(V8Debugger dbg) { |
| this.dbg = dbg; |
| this.sourceChangeListener = new SourceChangeListener(); |
| this.sourceChangeRoots = dbg.getScriptsHandler().getLocalRoots(); |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("new ChangeLiveSupport(), sourceChangeRoots = "+Arrays.toString(sourceChangeRoots)); |
| } |
| if (sourceChangeRoots.length == 0) { |
| FileUtil.addFileChangeListener(sourceChangeListener); |
| } else { |
| for (File root : sourceChangeRoots) { |
| FileUtil.addRecursiveListener(sourceChangeListener, root); |
| } |
| } |
| dbg.addListener(new V8Debugger.Listener() { |
| @Override public void notifySuspended(boolean suspended) {} |
| |
| @Override public void notifyCurrentFrame(CallFrame cf) {} |
| |
| @Override public void notifyFinished() { |
| destroy(); |
| } |
| }); |
| } |
| |
| public boolean hasChanges() { |
| return haveChanges; |
| } |
| |
| private void setHasChanges(boolean haveChanges) { |
| if (haveChanges == this.haveChanges) { |
| return ; |
| } |
| this.haveChanges = haveChanges; |
| pcl.firePropertyChange(PROP_CHANGES, !haveChanges, haveChanges); |
| } |
| |
| public void applyChanges() { |
| fileChangeDelivery.applyChanges(); |
| } |
| |
| public void addPropertyChangeListener(PropertyChangeListener l) { |
| pcl.addPropertyChangeListener(l); |
| } |
| |
| public void removePropertyChangeListener(PropertyChangeListener l) { |
| pcl.removePropertyChangeListener(l); |
| } |
| |
| private void applyModifiedFiles(List<FileObject> modifiedFiles) { |
| ScriptsHandler sh = dbg.getScriptsHandler(); |
| Collection<V8Script> scripts = sh.getScripts(); |
| final AtomicBoolean doStepInto = new AtomicBoolean(false); |
| final Phaser phaser = new Phaser(1); |
| LOG.log(Level.FINE, "applyModifiedFiles({0})", modifiedFiles); |
| for (FileObject fo : modifiedFiles) { |
| if (!sh.containsLocalFile(fo)) { |
| continue; |
| } |
| String path = fo.getPath(); |
| String serverPath; |
| try { |
| serverPath = sh.getServerPath(path); |
| } catch (ScriptsHandler.OutOfScope ex) { |
| continue; |
| } |
| V8Script script = null; |
| for (V8Script s : scripts) { |
| if (serverPath.equals(s.getName())) { |
| script = s; |
| break; |
| } |
| } |
| if (script == null) { |
| // Not a loaded script |
| continue; |
| } |
| String origScriptSource = script.getSource(); |
| if (origScriptSource == null) { |
| origScriptSource = script.getSourceStart(); |
| } |
| String prependedText = null; |
| Matcher matcher = PREP_PATTERN.matcher(origScriptSource); |
| if (matcher.matches()) { |
| int gc = matcher.groupCount(); |
| if (gc > 0) { |
| prependedText = matcher.group(1); |
| } |
| } |
| String fileSource; |
| try { |
| fileSource = fo.asText(); |
| } catch (IOException ioex) { |
| // Can not update scripts that can not be read. |
| continue; |
| } |
| LOG.fine("Identified changed script "+script.getName()); |
| if (prependedText != null) { |
| if (!fileSource.startsWith(prependedText)) { |
| // It's not there already |
| fileSource = prependedText + fileSource + APP_TEXT; |
| LOG.log(Level.FINE,"Header text added: ''{0}"+"'', appended: ''"+APP_TEXT+"''", prependedText); |
| } |
| } |
| V8Arguments changeLiveArgs = new ChangeLive.Arguments( |
| script.getId(), |
| fileSource, |
| Boolean.FALSE |
| ); |
| phaser.register(); |
| LOG.log(Level.FINE, "Running ChangeLive command for script {0}", script.getName()); |
| V8Request sendCLRequest = dbg.sendCommandRequest(V8Command.Changelive, changeLiveArgs, new V8Debugger.CommandResponseCallback() { |
| @Override |
| public void notifyResponse(V8Request request, V8Response response) { |
| try { |
| if (response != null) { |
| ChangeLive.ResponseBody clrb = (ChangeLive.ResponseBody) response.getBody(); |
| if (clrb != null) { |
| ChangeLive.ChangeLog changeLog = clrb.getChangeLog(); |
| if (changeLog != null) { |
| updateBreakpoints(changeLog.getBreakpointsUpdate()); |
| } |
| PropertyBoolean doStepIn = clrb.getStepInRecommended(); |
| ChangeLive.Result result = clrb.getResult(); |
| if (result != null) { |
| if (!doStepIn.hasValue()) { |
| doStepIn = result.getStackUpdateNeedsStepIn(); |
| } |
| } |
| if (doStepIn.getValue()) { |
| doStepInto.set(true); |
| } |
| } |
| } |
| } finally { |
| LOG.fine("A ChangeLive command finished."); |
| phaser.arriveAndDeregister(); |
| } |
| } |
| }); |
| if (sendCLRequest == null) { |
| phaser.arriveAndDeregister(); |
| } |
| } |
| phaser.arriveAndAwaitAdvance(); |
| boolean doStepIn = doStepInto.get(); |
| doStepIn = doStepIn && dbg.isSuspended(); |
| LOG.log(Level.FINE, "ALl ChangeLive commands processed. Will step into = {0}", doStepIn); |
| if (doStepIn) { |
| final CountDownLatch cdl = new CountDownLatch(1); |
| Continue.Arguments ca = new Continue.Arguments(V8StepAction.in); |
| V8Request stepInRequest = dbg.sendCommandRequest(V8Command.Continue, ca, new V8Debugger.CommandResponseCallback() { |
| @Override public void notifyResponse(V8Request request, V8Response response) { |
| if (response != null) { |
| dbg.addListener(new V8Debugger.Listener() { |
| |
| @Override public void notifySuspended(boolean suspended) { |
| if (suspended) { |
| cdl.countDown(); |
| dbg.removeListener(this); |
| } |
| } |
| |
| @Override public void notifyCurrentFrame(CallFrame cf) {} |
| |
| @Override public void notifyFinished() { |
| cdl.countDown(); |
| dbg.removeListener(this); |
| } |
| }); |
| } else { |
| cdl.countDown(); |
| } |
| } |
| }); |
| if (stepInRequest != null) { |
| try { |
| cdl.await(); |
| } catch (InterruptedException ex) {} |
| } |
| } |
| } |
| |
| private void updateBreakpoints(BreakpointUpdate[] breakpointsUpdate) { |
| for (BreakpointUpdate bu : breakpointsUpdate) { |
| long bpId = bu.getId(); |
| BreakpointUpdate.Type type = bu.getType(); |
| LOG.fine("updateBreakpoint id = "+bpId+", type = "+type); |
| switch (type) { |
| case CopiedToOld: |
| |
| break; |
| case PositionChanged: |
| Position oldPos = bu.getOldPositions(); |
| Position newPos = bu.getNewPositions(); |
| dbg.getBreakpointsHandler().positionChanged(bpId, |
| newPos.getLine(), newPos.getColumn()); |
| break; |
| } |
| } |
| } |
| |
| private void destroy() { |
| if (sourceChangeRoots.length == 0) { |
| FileUtil.removeFileChangeListener(sourceChangeListener); |
| } else { |
| for (File root : sourceChangeRoots) { |
| FileUtil.removeRecursiveListener(sourceChangeListener, root); |
| } |
| } |
| } |
| |
| private final class SourceChangeListener implements FileChangeListener { |
| |
| @Override |
| public void fileFolderCreated(FileEvent fe) { |
| } |
| |
| @Override |
| public void fileDataCreated(FileEvent fe) { |
| } |
| |
| @Override |
| public void fileChanged(FileEvent fe) { |
| fileChangeDelivery.add(fe.getFile()); |
| fe.runWhenDeliveryOver(fileChangeDelivery); |
| } |
| |
| @Override |
| public void fileDeleted(FileEvent fe) { |
| } |
| |
| @Override |
| public void fileRenamed(FileRenameEvent fe) { |
| } |
| |
| @Override |
| public void fileAttributeChanged(FileAttributeEvent fe) { |
| } |
| |
| } |
| |
| private final class FileChangeDelivery implements Runnable { |
| |
| private final List<FileObject> changedFiles = new LinkedList<>(); |
| |
| private void add(FileObject file) { |
| synchronized (changedFiles) { |
| changedFiles.add(file); |
| } |
| setHasChanges(true); |
| } |
| |
| @Override |
| public void run() { |
| if (DebuggerOptions.getInstance().isLiveEdit()) { |
| applyChanges(); |
| } |
| } |
| |
| void applyChanges() { |
| final List<FileObject> modifiedFiles; |
| synchronized (changedFiles) { |
| modifiedFiles = new ArrayList<>(changedFiles); |
| changedFiles.clear(); |
| } |
| setHasChanges(false); |
| rp.post(new Runnable() { |
| @Override |
| public void run() { |
| applyModifiedFiles(modifiedFiles); |
| } |
| }); |
| } |
| |
| } |
| } |