/** * erlyberly, erlang trace debugger * Copyright (C) 2016 Andy Till * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package erlyberly; import java.awt.Desktop; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.stream.Collectors; import com.ericsson.otp.erlang.OtpErlangAtom; import com.ericsson.otp.erlang.OtpErlangException; import com.ericsson.otp.erlang.OtpErlangList; import com.ericsson.otp.erlang.OtpErlangLong; import com.ericsson.otp.erlang.OtpErlangObject; import com.ericsson.otp.erlang.OtpErlangTuple; import erlyberly.node.NodeAPI; import erlyberly.node.OtpUtil; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.scene.control.ContextMenu; import javafx.scene.control.MenuItem; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.TreeItem; import javafx.scene.input.KeyCombination; public class ModFuncContextMenu extends ContextMenu { private static final String VIEW_SOURCE_CODE = "View Source Code"; private static final String VIEW_ABST_CODE = "View Abstract Code"; private final DbgController dbgController; private final SimpleObjectProperty<ModFunc> selectedItem; private final SimpleObjectProperty<TreeItem<ModFunc>> selectedTreeItem; private final SimpleBooleanProperty isSelectionModule, isSelectionFunction; /** * Root tree item of the module tree. */ private final SimpleObjectProperty<TreeItem<ModFunc>> rootProperty; private MenuItem moduleTraceMenuItem; public SimpleObjectProperty<TreeItem<ModFunc>> selectedTreeItemProperty() { return selectedTreeItem; } public SimpleObjectProperty<ModFunc> selectedItemProperty() { return selectedItem; } public SimpleObjectProperty<TreeItem<ModFunc>> rootProperty() { return rootProperty; } public ModFuncContextMenu(DbgController aDbgController) { isSelectionModule = new SimpleBooleanProperty(); isSelectionFunction = new SimpleBooleanProperty(); rootProperty = new SimpleObjectProperty<>(); dbgController = aDbgController; selectedItem = new SimpleObjectProperty<>(); selectedTreeItem = new SimpleObjectProperty<>(); selectedItem.addListener((o, oldv, newv) -> { if(newv == null) { isSelectionModule.set(false); isSelectionFunction.set(false); } else { boolean module = newv.isModule(); isSelectionModule.set(module); isSelectionFunction.set(!module); } }); MenuItem functionTraceMenuItem, exportsTraceMenuItem, traceAllMenuItem, seqTraceMenuItem, callGraphMenuItem, moduleSourceCodeItem, moduleAbstCodeItem; functionTraceMenuItem = new MenuItem("Function Trace"); functionTraceMenuItem.setOnAction(this::onFunctionTrace); functionTraceMenuItem.setAccelerator(KeyCombination.keyCombination("shortcut+t")); functionTraceMenuItem.disableProperty().bind(isSelectionFunction.not()); exportsTraceMenuItem = new MenuItem("Exported Function Trace"); exportsTraceMenuItem.setOnAction(this::onExportedFunctionTrace); exportsTraceMenuItem.setAccelerator(KeyCombination.keyCombination("shortcut+e")); traceAllMenuItem = new MenuItem("Trace All"); traceAllMenuItem.setOnAction((e) -> { toggleTracesToAllFunctions(); }); traceAllMenuItem.disableProperty().bind(rootProperty.isNull()); traceAllMenuItem.setAccelerator(KeyCombination.keyCombination("shortcut+shift+t")); moduleTraceMenuItem = new MenuItem("Recursive Trace"); moduleTraceMenuItem.setOnAction(this::onModuleTrace); seqTraceMenuItem = new MenuItem("Seq Trace (experimental)"); seqTraceMenuItem.setOnAction(this::onSeqTrace); seqTraceMenuItem.disableProperty().bind(isSelectionFunction.not()); BooleanBinding any = isSelectionFunction.or(isSelectionModule); moduleSourceCodeItem = new MenuItem(VIEW_SOURCE_CODE); moduleSourceCodeItem.setOnAction(this::onModuleCode); moduleSourceCodeItem.disableProperty().bind(any.not()); moduleAbstCodeItem = new MenuItem(VIEW_ABST_CODE); moduleAbstCodeItem.setOnAction(this::onModuleCode); moduleAbstCodeItem.disableProperty().bind(any.not()); NodeAPI nodeAPI = ErlyBerly.nodeAPI(); SimpleBooleanProperty connectedProperty = nodeAPI.connectedProperty(); callGraphMenuItem = new MenuItem("View Call Graph"); callGraphMenuItem.setOnAction(this::onViewCallGraph); callGraphMenuItem.disableProperty().bind(connectedProperty.not().or(nodeAPI.xrefStartedProperty().not()).or(isSelectionFunction.not())); getItems().addAll( functionTraceMenuItem, exportsTraceMenuItem, traceAllMenuItem, moduleTraceMenuItem, seqTraceMenuItem, new SeparatorMenuItem(), callGraphMenuItem, new SeparatorMenuItem(), moduleSourceCodeItem, moduleAbstCodeItem); } private void onSeqTrace(ActionEvent ae) { ModFunc func = selectedItem.get(); if(func == null) return; dbgController.seqTrace(func); ErlyBerly.showPane("Seq Trace", new SeqTraceView(dbgController.getSeqTraceLogs())); } private void onViewCallGraph(ActionEvent ae) { ModFunc func = selectedItem.get(); if(func == null) return; if(func.isModule()) return; List<String> defaultSkippedModules = Arrays.asList( "app_helper", "application", "binary", "code", "compile", "crypto", "dets", "dict", "erl_pp", "erl_syntax", "erlang", "ets", "exometer", "exometer_admin", "file", "gb_sets", "gen", "gen_event", "gen_fsm", "gen_server", "httpc", "httpd_util", "inet", "inet_parse", "inets", "io", "io_lib", "io_lib_pretty", "lager", "lager_config", "lists", "mnesia", "msgpack", "orddict", "proplists", "sets", "string", "timer" ); List<String> skippedModules = (List<String>) PrefBind.getOrDefault("xrefSkippedModules", defaultSkippedModules); List<OtpErlangAtom> skippedModuleAtoms = skippedModules.stream().map(OtpUtil::atom).collect(Collectors.toList()); try { OtpErlangObject callGraph = ErlyBerly.nodeAPI().callGraph( new OtpErlangList(skippedModuleAtoms.toArray(new OtpErlangAtom[]{})), OtpUtil.atom(func.getModuleName()), OtpUtil.atom(func.getFuncName()), new OtpErlangLong(func.getArity())); CallGraphView callGraphView = new CallGraphView(dbgController); callGraphView.callGraph((OtpErlangTuple) callGraph); ErlyBerly.showPane(func.toFullString() + " call graph", ErlyBerly.wrapInPane(callGraphView)); } catch (OtpErlangException | IOException e) { e.printStackTrace(); } } private void onFunctionTrace(ActionEvent e) { ModFunc mf = selectedItem.get(); if(selectedItem == null) return; dbgController.toggleTraceModFunc(mf); } private void onModuleTrace(ActionEvent e) { TreeItem<ModFunc> selectedItem = selectedTreeItemProperty().get(); if(selectedItem == null) return; if(selectedItem.getValue() == null) return; if(!selectedItem.getValue().isModule()) selectedItem = selectedItem.getParent(); HashSet<ModFunc> funcs = new HashSet<ModFunc>(); recurseModFuncItems(selectedItem, funcs); toggleTraceMod(funcs); } private void onExportedFunctionTrace(ActionEvent e) { TreeItem<ModFunc> selectedItem = selectedTreeItemProperty().get(); if(selectedItem == null) return; if(selectedItem.getValue() == null) return; if(!selectedItem.getValue().isModule()) selectedItem = selectedItem.getParent(); // get all the functions we may trace HashSet<ModFunc> funcs = new HashSet<ModFunc>(); recurseModFuncItems(selectedItem, funcs); // filter the exported ones HashSet<ModFunc> exported = new HashSet<ModFunc>(); for (ModFunc modFunc : funcs) { if(modFunc.isExported()) { exported.add(modFunc); } } // trace 'em toggleTraceMod(exported); } private void recurseModFuncItems(TreeItem<ModFunc> item, HashSet<ModFunc> funcs) { if(item == null) return; if(item.getValue() == null || (!item.getValue().isModule() && !item.getValue().isModuleInfo())) funcs.add(item.getValue()); for (TreeItem<ModFunc> childItem : item.getChildren()) { recurseModFuncItems(childItem, funcs); } } private void toggleTraceMod(Collection<ModFunc> functions){ for (ModFunc func : functions) { dbgController.toggleTraceModFunc(func); } } private void onModuleCode(ActionEvent ae){ try{ MenuItem mi = (MenuItem) ae.getSource(); String menuItemClicked = mi.getText(); ModFunc mf = selectedItem.get(); if(mf == null) return; String moduleName = mf.getModuleName(); String modSrc; if(mf.isModule()) { modSrc = fetchModCode(menuItemClicked, moduleName); showModuleSourceCode(moduleName + " Source code ", modSrc); } else { String functionName = mf.getFuncName(); Integer arity = mf.getArity(); modSrc = fetchModCode(menuItemClicked, moduleName, functionName, arity); showModuleSourceCode(moduleName, modSrc); } } catch (Exception e) { throw new RuntimeException("failed to load the source code.", e); } } private String fetchModCode(String menuItemClicked, String moduleName) throws IOException, OtpErlangException { switch (menuItemClicked){ case VIEW_SOURCE_CODE: return dbgController.moduleFunctionSourceCode(moduleName); case VIEW_ABST_CODE: return dbgController.moduleFunctionAbstCode(moduleName); default: return ""; } } private String fetchModCode(String menuItemClicked, String moduleName, String function, Integer arity) throws IOException, OtpErlangException { switch (menuItemClicked){ case VIEW_SOURCE_CODE: return dbgController.moduleFunctionSourceCode(moduleName, function, arity); case VIEW_ABST_CODE: return dbgController.moduleFunctionAbstCode(moduleName, function, arity); default: return ""; } } private void showModuleSourceCode(String title, String moduleSourceCode) { Boolean showSourceInSystemEditor = PrefBind.getOrDefaultBoolean("showSourceInSystemEditor", false); if(showSourceInSystemEditor) { try { File tmpSourceFile = File.createTempFile(title, ".erl"); FileOutputStream out = new FileOutputStream(tmpSourceFile); out.write(moduleSourceCode.getBytes()); out.close(); Desktop.getDesktop().edit(tmpSourceFile); } catch (IOException e) { e.printStackTrace(); ErlyBerly.showPane(title, ErlyBerly.wrapInPane(new CodeView(moduleSourceCode))); } } else { ErlyBerly.showPane(title, ErlyBerly.wrapInPane(new CodeView(moduleSourceCode))); } } /** * Toggle tracing on all unfiltered (visible, even unexpanded) functions. */ public void toggleTracesToAllFunctions() { ArrayList<ModFunc> funs = findUnfilteredFunctions(); for (ModFunc func : funs) { dbgController.toggleTraceModFunc(func); } } private ArrayList<ModFunc> findUnfilteredFunctions() { ObservableList<TreeItem<ModFunc>> filteredTreeModules = rootProperty.get().getChildren(); ArrayList<ModFunc> funs = new ArrayList<>(); for (TreeItem<ModFunc> treeItem : filteredTreeModules) { for (TreeItem<ModFunc> modFunc : treeItem.getChildren()) { if(!modFunc.getValue().isModuleInfo()) { funs.add(modFunc.getValue()); } } } return funs; } public void setModuleTraceMenuText(String menuText) { moduleTraceMenuItem.setText(menuText); } }