From 0f95603cfd69341b7819d34f1b1d1827d2323a44 Mon Sep 17 00:00:00 2001 From: Harsh Date: Sun, 19 Oct 2025 16:35:47 +0530 Subject: [PATCH 1/4] Add diagnostics gathering feature to debugger and UI --- build/shared/lib/languages/PDE.properties | 1 + core/src/processing/core/PApplet.java | 15 ++ core/src/processing/core/PDiagnostics.java | 188 +++++++++++++++ .../processing/mode/java/debug/Debugger.java | 96 ++++++++ .../mode/java/debug/DiagnosticsDialog.java | 217 ++++++++++++++++++ 5 files changed, 517 insertions(+) create mode 100644 core/src/processing/core/PDiagnostics.java create mode 100644 java/src/processing/mode/java/debug/DiagnosticsDialog.java diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index 19a5c9f866..ca77e1968b 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -125,6 +125,7 @@ menu.debug.continue = Continue #menu.debug.variable_inspector = Variable Inspector menu.debug.show_variables = Show Variables menu.debug.hide_variables = Hide Variables +menu.debug.gather_diagnostics = Gather Diagnostics #menu.debug.show_sketch_outline = Show Sketch Outline #menu.debug.show_tabs_list = Show Tabs List diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java index 4fccd1a535..c74441cc06 100644 --- a/core/src/processing/core/PApplet.java +++ b/core/src/processing/core/PApplet.java @@ -4011,6 +4011,21 @@ static public void debug(String msg) { } // + /** + * Gathers diagnostic information about the sketch and system. + *

+ * This method is intended to be called remotely via the debugger + * to collect system information, memory statistics, and sketch + * runtime details for troubleshooting purposes. + *

+ * + * @return JSON string containing diagnostic information + * @since 4.4 + */ + public String getDiagnostics() { + return PDiagnostics.gather(this); + } + /* // not very useful, because it only works for public (and protected?) // fields of a class, not local variables to methods diff --git a/core/src/processing/core/PDiagnostics.java b/core/src/processing/core/PDiagnostics.java new file mode 100644 index 0000000000..a38d6bb4b0 --- /dev/null +++ b/core/src/processing/core/PDiagnostics.java @@ -0,0 +1,188 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-25 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + +import java.awt.DisplayMode; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.text.SimpleDateFormat; +import java.util.Date; + + +/** + * System diagnostics gathering for Processing sketches. + *

+ * This class collects system information, Processing runtime details, + * and memory statistics for debugging purposes. It's designed to be + * called from the PDE debugger over the debug connection. + *

+ * + * @author Processing Foundation + * @since 4.4 + */ +public class PDiagnostics { + + // Privacy-safe system properties to include + private static final String[] SAFE_PROPERTIES = { + "java.version", + "java.vendor", + "java.vm.name", + "java.vm.version", + "java.runtime.name", + "java.runtime.version", + "os.name", + "os.version", + "os.arch", + "file.separator", + "path.separator", + "line.separator" + }; + + + /** + * Gathers diagnostic information from a running sketch. + * + * @param applet the PApplet instance to gather diagnostics from + * @return JSON string containing diagnostic information + */ + public static String gather(PApplet applet) { + StringBuilder json = new StringBuilder(); + json.append("{\n"); + + // Timestamp + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + json.append(" \"timestamp\": \"").append(dateFormat.format(new Date())).append("\",\n"); + + // Processing version (read from system property, set by PDE at launch) + String processingVersion = System.getProperty("processing.version", "unknown"); + json.append(" \"processingVersion\": ").append(jsonString(processingVersion)).append(",\n"); + + // System properties + json.append(" \"system\": {\n"); + boolean first = true; + for (String prop : SAFE_PROPERTIES) { + String value = System.getProperty(prop); + if (value != null) { + if (!first) json.append(",\n"); + // Sanitize line separator for JSON + if (prop.equals("line.separator")) { + value = escapeForJson(value); + } + json.append(" \"").append(prop).append("\": ").append(jsonString(value)); + first = false; + } + } + json.append("\n },\n"); + + // Memory information + Runtime runtime = Runtime.getRuntime(); + json.append(" \"memory\": {\n"); + json.append(" \"totalMemory\": ").append(runtime.totalMemory()).append(",\n"); + json.append(" \"freeMemory\": ").append(runtime.freeMemory()).append(",\n"); + json.append(" \"maxMemory\": ").append(runtime.maxMemory()).append(",\n"); + json.append(" \"usedMemory\": ").append(runtime.totalMemory() - runtime.freeMemory()).append("\n"); + json.append(" },\n"); + + // Sketch-specific information + if (applet != null) { + json.append(" \"sketch\": {\n"); + + // Renderer information + PGraphics graphics = applet.g; + if (graphics != null) { + json.append(" \"renderer\": ").append(jsonString(graphics.getClass().getSimpleName())).append(",\n"); + json.append(" \"width\": ").append(applet.width).append(",\n"); + json.append(" \"height\": ").append(applet.height).append(",\n"); + json.append(" \"pixelDensity\": ").append(applet.pixelDensity).append(",\n"); + json.append(" \"frameCount\": ").append(applet.frameCount).append(",\n"); + json.append(" \"frameRate\": ").append(String.format("%.2f", applet.frameRate)).append(",\n"); + } + + json.append(" \"focused\": ").append(applet.focused).append("\n"); + json.append(" },\n"); + } + + // Display information + try { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] devices = ge.getScreenDevices(); + + json.append(" \"displays\": [\n"); + for (int i = 0; i < devices.length; i++) { + if (i > 0) json.append(",\n"); + GraphicsDevice device = devices[i]; + DisplayMode mode = device.getDisplayMode(); + + json.append(" {\n"); + json.append(" \"id\": ").append(i).append(",\n"); + json.append(" \"width\": ").append(mode.getWidth()).append(",\n"); + json.append(" \"height\": ").append(mode.getHeight()).append(",\n"); + json.append(" \"refreshRate\": ").append(mode.getRefreshRate()).append(",\n"); + json.append(" \"bitDepth\": ").append(mode.getBitDepth()).append("\n"); + json.append(" }"); + } + json.append("\n ]\n"); + + } catch (Exception e) { + json.append(" \"displays\": \"Error: ").append(e.getMessage()).append("\"\n"); + } + + json.append("}"); + return json.toString(); + } + + + /** + * Converts a string to JSON-safe quoted string. + */ + private static String jsonString(String value) { + if (value == null) { + return "null"; + } + return "\"" + escapeForJson(value) + "\""; + } + + + /** + * Escapes special characters for JSON. + */ + private static String escapeForJson(String value) { + return value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + + /** + * Formats bytes into human-readable format (KB, MB, GB). + */ + public static String formatBytes(long bytes) { + if (bytes < 1024) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(1024)); + char unit = "KMGT".charAt(exp - 1); + return String.format("%.2f %sB", bytes / Math.pow(1024, exp), unit); + } +} diff --git a/java/src/processing/mode/java/debug/Debugger.java b/java/src/processing/mode/java/debug/Debugger.java index 0136793200..59fb747ee5 100644 --- a/java/src/processing/mode/java/debug/Debugger.java +++ b/java/src/processing/mode/java/debug/Debugger.java @@ -173,6 +173,16 @@ public void actionPerformed(ActionEvent e) { debugMenu.addSeparator(); + item = Toolkit.newJMenuItemExt("menu.debug.gather_diagnostics"); + item.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Messages.log("Invoked 'Gather Diagnostics' menu item"); + gatherDiagnostics(); + } + }); + debugMenu.add(item); + item.setEnabled(false); + item = Toolkit.newJMenuItem(Language.text("menu.debug.toggle_breakpoint"), 'B'); item.addActionListener(new ActionListener() { @@ -458,6 +468,92 @@ public synchronized void stepOut() { } + /** + * Gather system diagnostics from the running sketch. + *

+ * Calls the getDiagnostics() method on the sketch's PApplet instance + * via JDWP and displays the results in a dialog. + *

+ */ + public synchronized void gatherDiagnostics() { + if (!isStarted()) { + editor.statusNotice("Sketch must be running in debug mode to gather diagnostics."); + return; + } + + if (!isPaused()) { + editor.statusNotice("Please pause the sketch first (at a breakpoint or use Continue/Step)."); + return; + } + + try { + if (currentThread == null || currentThread.frameCount() == 0) { + editor.statusError("No valid stack frame available."); + return; + } + + ObjectReference appletRef = currentThread.frame(0).thisObject(); + if (appletRef == null) { + editor.statusError("Could not find PApplet instance."); + return; + } + + ReferenceType appletType = appletRef.referenceType(); + if (!(appletType instanceof ClassType)) { + editor.statusError("PApplet reference is not a ClassType."); + return; + } + + Method diagnosticsMethod = ((ClassType) appletType).concreteMethodByName("getDiagnostics", "()Ljava/lang/String;"); + + if (diagnosticsMethod == null) { + editor.statusError("getDiagnostics() method not found. Make sure core library is up to date."); + return; + } + + editor.statusNotice("Gathering diagnostics from sketch..."); + Value resultValue = appletRef.invokeMethod( + currentThread, + diagnosticsMethod, + new ArrayList<>(), + ObjectReference.INVOKE_SINGLE_THREADED + ); + + if (resultValue == null) { + editor.statusError("getDiagnostics() returned null."); + return; + } + + if (!(resultValue instanceof StringReference)) { + editor.statusError("getDiagnostics() returned unexpected type: " + resultValue.getClass().getName()); + return; + } + + StringReference result = (StringReference) resultValue; + String diagnosticData = result.value(); + + javax.swing.SwingUtilities.invokeLater(new Runnable() { + public void run() { + DiagnosticsDialog dialog = new DiagnosticsDialog(editor, diagnosticData); + dialog.setVisible(true); + } + }); + + editor.statusNotice("Diagnostics gathered successfully."); + + } catch (IncompatibleThreadStateException e) { + editor.statusError("Thread is not suspended."); + logitse(e); + } catch (ClassCastException e) { + editor.statusError("Unexpected return type from getDiagnostics()."); + e.printStackTrace(); + } catch (Exception e) { + editor.statusError("Error gathering diagnostics: " + e.getMessage()); + e.printStackTrace(); + } + } + + // /** Print the current stack trace. */ // public synchronized void printStackTrace() { // if (isStarted()) { diff --git a/java/src/processing/mode/java/debug/DiagnosticsDialog.java b/java/src/processing/mode/java/debug/DiagnosticsDialog.java new file mode 100644 index 0000000000..a44cd3ab2b --- /dev/null +++ b/java/src/processing/mode/java/debug/DiagnosticsDialog.java @@ -0,0 +1,217 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + Copyright (c) 2012-25 The Processing Foundation + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License version 2 + as published by the Free Software Foundation. + + 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, write to the Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +package processing.mode.java.debug; + +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; + +import processing.app.Language; +import processing.mode.java.JavaEditor; + + +/** + * Dialog for displaying system diagnostics gathered from a running sketch. + *

+ * Provides a formatted view of diagnostic information including system + * properties, memory statistics, sketch details, and display information. + * Users can copy the diagnostics to clipboard or export to a text file. + *

+ * + * @author Processing Foundation + * @since 4.4 + */ +public class DiagnosticsDialog extends JDialog { + private JTextArea textArea; + private String diagnosticData; + private JavaEditor editor; + + + public DiagnosticsDialog(JavaEditor editor, String diagnosticData) { + super(editor, Language.text("menu.debug.gather_diagnostics"), false); + this.editor = editor; + this.diagnosticData = diagnosticData; + + setLayout(new BorderLayout()); + + textArea = new JTextArea(); + textArea.setEditable(false); + textArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); + textArea.setText(formatDiagnostics(diagnosticData)); + textArea.setCaretPosition(0); + + JScrollPane scrollPane = new JScrollPane(textArea); + scrollPane.setPreferredSize(new Dimension(600, 500)); + add(scrollPane, BorderLayout.CENTER); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + buttonPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); + + JButton copyButton = new JButton(Language.text("menu.edit.copy")); + copyButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + copyToClipboard(); + } + }); + buttonPanel.add(copyButton); + + JButton exportButton = new JButton(Language.text("prompt.export")); + exportButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + exportToFile(); + } + }); + buttonPanel.add(exportButton); + + JButton closeButton = new JButton(Language.text("menu.file.close")); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + dispose(); + } + }); + buttonPanel.add(closeButton); + + add(buttonPanel, BorderLayout.SOUTH); + + pack(); + setLocationRelativeTo(editor); + } + + + /** + * Formats the JSON diagnostic data into a human-readable format. + */ + private String formatDiagnostics(String jsonData) { + if (jsonData == null || jsonData.isEmpty()) { + return "No diagnostic data available."; + } + + StringBuilder formatted = new StringBuilder(); + formatted.append("System Diagnostics\n"); + formatted.append("==================\n\n"); + + String[] lines = jsonData.split("\n"); + int indentLevel = 0; + + for (String line : lines) { + String trimmed = line.trim(); + + if (trimmed.startsWith("}") || trimmed.startsWith("]")) { + indentLevel--; + } + + if (trimmed.length() > 0 && !trimmed.equals("{") && !trimmed.equals("}") + && !trimmed.equals("[") && !trimmed.equals("]") + && !trimmed.equals("},") && !trimmed.equals("],")) { + + String display = trimmed.replaceAll("\"", "") + .replaceAll(",", "") + .replaceAll(":", ": "); + + for (int i = 0; i < indentLevel; i++) { + formatted.append(" "); + } + + formatted.append(display).append("\n"); + } + + if (trimmed.startsWith("{") || trimmed.startsWith("[")) { + if (trimmed.contains(":")) { + String sectionName = trimmed.substring(0, trimmed.indexOf(":")); + sectionName = sectionName.replaceAll("\"", ""); + formatted.append("\n").append(sectionName.toUpperCase()).append(":\n"); + } + indentLevel++; + } + } + + formatted.append("\n"); + formatted.append("==================\n"); + formatted.append("Generated: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("\n"); + + return formatted.toString(); + } + + + /** + * Copies diagnostic data to system clipboard. + */ + private void copyToClipboard() { + try { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + StringSelection selection = new StringSelection(textArea.getText()); + clipboard.setContents(selection, null); + + editor.statusNotice("Diagnostics copied to clipboard."); + } catch (Exception e) { + JOptionPane.showMessageDialog(this, + "Could not copy diagnostics to clipboard.\n" + e.getMessage(), + "Copy Failed", + JOptionPane.WARNING_MESSAGE); + } + } + + + /** + * Exports diagnostic data to a text file. + */ + private void exportToFile() { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle("Save Diagnostics"); + + // Default filename with timestamp + String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + fileChooser.setSelectedFile(new File("diagnostics_" + timestamp + ".txt")); + + int result = fileChooser.showSaveDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + File file = fileChooser.getSelectedFile(); + + try (FileWriter writer = new FileWriter(file)) { + writer.write(textArea.getText()); + writer.write("\n\n"); + writer.write("Raw JSON Data:\n"); + writer.write("==============\n"); + writer.write(diagnosticData); + + editor.statusNotice("Diagnostics exported to " + file.getName()); + } catch (IOException e) { + JOptionPane.showMessageDialog(this, + "Could not export diagnostics to file.\n" + e.getMessage(), + "Export Failed", + JOptionPane.WARNING_MESSAGE); + } + } + } +} From b3add5c8c8b742a0e843f6b9a038ed2800f6dd75 Mon Sep 17 00:00:00 2001 From: Harsh Date: Sun, 19 Oct 2025 20:33:57 +0530 Subject: [PATCH 2/4] Improve error messaging in Debugger and optimize token skipping in DiagnosticsDialog --- java/src/processing/mode/java/debug/Debugger.java | 13 +++++++++++-- .../mode/java/debug/DiagnosticsDialog.java | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/java/src/processing/mode/java/debug/Debugger.java b/java/src/processing/mode/java/debug/Debugger.java index 59fb747ee5..9f4a9aa410 100644 --- a/java/src/processing/mode/java/debug/Debugger.java +++ b/java/src/processing/mode/java/debug/Debugger.java @@ -494,7 +494,8 @@ public synchronized void gatherDiagnostics() { ObjectReference appletRef = currentThread.frame(0).thisObject(); if (appletRef == null) { - editor.statusError("Could not find PApplet instance."); + editor.statusError("Could not find PApplet instance. This may happen if the sketch " + + "is paused in a static method. Try pausing in an instance method instead."); return; } @@ -504,7 +505,15 @@ public synchronized void gatherDiagnostics() { return; } - Method diagnosticsMethod = ((ClassType) appletType).concreteMethodByName("getDiagnostics", "()Ljava/lang/String;"); + // Walk the class hierarchy to find the getDiagnostics method + Method diagnosticsMethod = null; + ClassType currentClass = (ClassType) appletType; + while (currentClass != null && diagnosticsMethod == null) { + diagnosticsMethod = currentClass.concreteMethodByName("getDiagnostics", "()Ljava/lang/String;"); + if (diagnosticsMethod == null) { + currentClass = currentClass.superclass(); + } + } if (diagnosticsMethod == null) { editor.statusError("getDiagnostics() method not found. Make sure core library is up to date."); diff --git a/java/src/processing/mode/java/debug/DiagnosticsDialog.java b/java/src/processing/mode/java/debug/DiagnosticsDialog.java index a44cd3ab2b..0da087f4a0 100644 --- a/java/src/processing/mode/java/debug/DiagnosticsDialog.java +++ b/java/src/processing/mode/java/debug/DiagnosticsDialog.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Set; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -123,6 +124,7 @@ private String formatDiagnostics(String jsonData) { String[] lines = jsonData.split("\n"); int indentLevel = 0; + Set skipTokens = Set.of("{", "}", "[", "]", "},", "],"); for (String line : lines) { String trimmed = line.trim(); @@ -131,9 +133,7 @@ private String formatDiagnostics(String jsonData) { indentLevel--; } - if (trimmed.length() > 0 && !trimmed.equals("{") && !trimmed.equals("}") - && !trimmed.equals("[") && !trimmed.equals("]") - && !trimmed.equals("},") && !trimmed.equals("],")) { + if (trimmed.length() > 0 && !skipTokens.contains(trimmed)) { String display = trimmed.replaceAll("\"", "") .replaceAll(",", "") From c52c49253b52fb8d17fdfb6485f9eda04d41fd38 Mon Sep 17 00:00:00 2001 From: Harsh Date: Wed, 17 Dec 2025 01:35:36 +0530 Subject: [PATCH 3/4] add suggested chaanges --- app/src/processing/app/ui/Editor.java | 9 ++ app/src/processing/app/ui/EditorFooter.java | 16 ++- java/src/processing/mode/java/JavaEditor.java | 10 ++ .../processing/mode/java/debug/Debugger.java | 118 ++++++++++++++++++ 4 files changed, 150 insertions(+), 3 deletions(-) diff --git a/app/src/processing/app/ui/Editor.java b/app/src/processing/app/ui/Editor.java index 0437240b37..c297486123 100644 --- a/app/src/processing/app/ui/Editor.java +++ b/app/src/processing/app/ui/Editor.java @@ -2945,4 +2945,13 @@ public void show(Component component, int x, int y) { super.show(component, x, y); } } + + /** + * Called when clicking on the version number in the footer. + * Return a string with diagnostic info from the sketch, + * or empty string (or null) if not implemented/available. + */ + public String getSketchDiagnostics() { + return ""; + } } diff --git a/app/src/processing/app/ui/EditorFooter.java b/app/src/processing/app/ui/EditorFooter.java index 94860a0abf..7efef4132e 100644 --- a/app/src/processing/app/ui/EditorFooter.java +++ b/app/src/processing/app/ui/EditorFooter.java @@ -109,7 +109,7 @@ public void mousePressed(MouseEvent e) { Base.DEBUG = !Base.DEBUG; editor.updateDevelopMenu(); } - copyDebugInformationToClipboard(); + copyFullDiagnosticsToClipboard(); } }); @@ -120,13 +120,23 @@ public void mousePressed(MouseEvent e) { updateTheme(); } - public static void copyDebugInformationToClipboard() { - var debugInformation = String.join("\n", + public static String getSystemDebugInformation() { + return String.join("\n", "Version: " + Base.getVersionName(), "Revision: " + Base.getRevision(), "OS: " + System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch"), "Java: " + System.getProperty("java.version") + " " + System.getProperty("java.vendor") ); + } + + public static void copyDebugInformationToClipboard() { + var stringSelection = new StringSelection(getSystemDebugInformation()); + var clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(stringSelection, null); + } + + public void copyFullDiagnosticsToClipboard() { + var debugInformation = getSystemDebugInformation() + "\n\n" + editor.getSketchDiagnostics(); var stringSelection = new StringSelection(debugInformation); var clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 8d9b2252c8..561e38ba54 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -2234,4 +2234,14 @@ static private int howManyFloats(List> handles) { } return count; } + + @Override + public String getSketchDiagnostics() { + if (debugger.isStarted()) { + return debugger.getDiagnostics(); + } else if (runtime != null) { + return Debugger.getDiagnostics(runtime); + } + return super.getSketchDiagnostics(); + } } diff --git a/java/src/processing/mode/java/debug/Debugger.java b/java/src/processing/mode/java/debug/Debugger.java index 9f4a9aa410..1c217f2f2b 100644 --- a/java/src/processing/mode/java/debug/Debugger.java +++ b/java/src/processing/mode/java/debug/Debugger.java @@ -31,6 +31,7 @@ import java.awt.event.KeyEvent; import java.io.*; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -468,6 +469,123 @@ public synchronized void stepOut() { } + + /** + * Get diagnostics from the sketch, whether paused or running. + * If running, it will temporarily suspend the VM. + */ + public String getDiagnostics() { + return getDiagnostics(runtime); + } + + + /** + * Static helper to fetch diagnostics from a Runner, even if not debugging. + * Uses field reads instead of method invocations to avoid thread state issues. + */ + public static String getDiagnostics(Runner targetRuntime) { + if (targetRuntime == null) return ""; + VirtualMachine targetVM = targetRuntime.vm(); + if (targetVM == null) return ""; + + targetVM.suspend(); + try { + // Find the PApplet subclass + List pAppletClasses = targetVM.classesByName("processing.core.PApplet"); + if (pAppletClasses.isEmpty()) { + return "processing.core.PApplet not found in VM"; + } + ClassType pAppletBase = (ClassType) pAppletClasses.get(0); + + ClassType sketchClass = null; + for (ReferenceType type : targetVM.allClasses()) { + if (type instanceof ClassType) { + ClassType ct = (ClassType) type; + ClassType superclass = ct.superclass(); + while (superclass != null) { + if (superclass.equals(pAppletBase)) { + sketchClass = ct; + break; + } + superclass = superclass.superclass(); + } + if (sketchClass != null) break; + } + } + + if (sketchClass == null) { + return "Could not find sketch class extending PApplet"; + } + + // Find instance + List instances = sketchClass.instances(1); + if (instances.isEmpty()) { + return "No instance of " + sketchClass.name() + " found"; + } + ObjectReference appletInstance = instances.get(0); + + // Build diagnostics by reading fields directly (no thread required) + StringBuilder diag = new StringBuilder(); + diag.append("Sketch Diagnostics:\n"); + diag.append(" Class: ").append(sketchClass.name()).append("\n"); + + // Read PApplet fields + appendField(diag, appletInstance, pAppletBase, "width"); + appendField(diag, appletInstance, pAppletBase, "height"); + appendField(diag, appletInstance, pAppletBase, "pixelDensity"); + appendField(diag, appletInstance, pAppletBase, "frameCount"); + appendField(diag, appletInstance, pAppletBase, "frameRate"); + appendField(diag, appletInstance, pAppletBase, "focused"); + + // Try to get renderer class name from 'g' field (PGraphics) + try { + Field gField = pAppletBase.fieldByName("g"); + if (gField != null) { + Value gValue = appletInstance.getValue(gField); + if (gValue instanceof ObjectReference) { + ObjectReference graphics = (ObjectReference) gValue; + diag.append(" renderer: ").append(graphics.referenceType().name()).append("\n"); + } + } + } catch (Exception e) { + diag.append(" renderer: (unavailable)\n"); + } + + return diag.toString(); + + } catch (Exception e) { + return "Error gathering diagnostics: " + e.toString(); + } finally { + targetVM.resume(); + } + } + + /** + * Helper to append a field value to the diagnostics string. + */ + private static void appendField(StringBuilder sb, ObjectReference obj, ClassType type, String fieldName) { + try { + Field field = type.fieldByName(fieldName); + if (field != null) { + Value value = obj.getValue(field); + sb.append(" ").append(fieldName).append(": "); + if (value == null) { + sb.append("null"); + } else if (value instanceof com.sun.jdi.PrimitiveValue) { + sb.append(value.toString()); + } else if (value instanceof StringReference) { + sb.append(((StringReference) value).value()); + } else { + sb.append(value.toString()); + } + sb.append("\n"); + } + } catch (Exception e) { + sb.append(" ").append(fieldName).append(": (error: ").append(e.getMessage()).append(")\n"); + } + } + + /** * Gather system diagnostics from the running sketch. *

From 1d1085ff8cd3e629d162a976a44fb54557fef4b5 Mon Sep 17 00:00:00 2001 From: Harsh Date: Wed, 17 Dec 2025 01:45:02 +0530 Subject: [PATCH 4/4] clean up --- .../main/resources/languages/PDE.properties | 1 - core/src/processing/core/PApplet.java | 15 -- core/src/processing/core/PDiagnostics.java | 188 --------------- .../processing/mode/java/debug/Debugger.java | 104 --------- .../mode/java/debug/DiagnosticsDialog.java | 217 ------------------ 5 files changed, 525 deletions(-) delete mode 100644 core/src/processing/core/PDiagnostics.java delete mode 100644 java/src/processing/mode/java/debug/DiagnosticsDialog.java diff --git a/app/src/main/resources/languages/PDE.properties b/app/src/main/resources/languages/PDE.properties index 0af0378eba..3c1ad2ab70 100644 --- a/app/src/main/resources/languages/PDE.properties +++ b/app/src/main/resources/languages/PDE.properties @@ -125,7 +125,6 @@ menu.debug.continue = Continue #menu.debug.variable_inspector = Variable Inspector menu.debug.show_variables = Show Variables menu.debug.hide_variables = Hide Variables -menu.debug.gather_diagnostics = Gather Diagnostics #menu.debug.show_sketch_outline = Show Sketch Outline #menu.debug.show_tabs_list = Show Tabs List diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java index c74441cc06..4fccd1a535 100644 --- a/core/src/processing/core/PApplet.java +++ b/core/src/processing/core/PApplet.java @@ -4011,21 +4011,6 @@ static public void debug(String msg) { } // - /** - * Gathers diagnostic information about the sketch and system. - *

- * This method is intended to be called remotely via the debugger - * to collect system information, memory statistics, and sketch - * runtime details for troubleshooting purposes. - *

- * - * @return JSON string containing diagnostic information - * @since 4.4 - */ - public String getDiagnostics() { - return PDiagnostics.gather(this); - } - /* // not very useful, because it only works for public (and protected?) // fields of a class, not local variables to methods diff --git a/core/src/processing/core/PDiagnostics.java b/core/src/processing/core/PDiagnostics.java deleted file mode 100644 index a38d6bb4b0..0000000000 --- a/core/src/processing/core/PDiagnostics.java +++ /dev/null @@ -1,188 +0,0 @@ -/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ - -/* - Part of the Processing project - http://processing.org - - Copyright (c) 2012-25 The Processing Foundation - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation, version 2.1. - - This library 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 - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General - Public License along with this library; if not, write to the - Free Software Foundation, Inc., 59 Temple Place, Suite 330, - Boston, MA 02111-1307 USA -*/ - -package processing.core; - -import java.awt.DisplayMode; -import java.awt.GraphicsDevice; -import java.awt.GraphicsEnvironment; -import java.text.SimpleDateFormat; -import java.util.Date; - - -/** - * System diagnostics gathering for Processing sketches. - *

- * This class collects system information, Processing runtime details, - * and memory statistics for debugging purposes. It's designed to be - * called from the PDE debugger over the debug connection. - *

- * - * @author Processing Foundation - * @since 4.4 - */ -public class PDiagnostics { - - // Privacy-safe system properties to include - private static final String[] SAFE_PROPERTIES = { - "java.version", - "java.vendor", - "java.vm.name", - "java.vm.version", - "java.runtime.name", - "java.runtime.version", - "os.name", - "os.version", - "os.arch", - "file.separator", - "path.separator", - "line.separator" - }; - - - /** - * Gathers diagnostic information from a running sketch. - * - * @param applet the PApplet instance to gather diagnostics from - * @return JSON string containing diagnostic information - */ - public static String gather(PApplet applet) { - StringBuilder json = new StringBuilder(); - json.append("{\n"); - - // Timestamp - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - json.append(" \"timestamp\": \"").append(dateFormat.format(new Date())).append("\",\n"); - - // Processing version (read from system property, set by PDE at launch) - String processingVersion = System.getProperty("processing.version", "unknown"); - json.append(" \"processingVersion\": ").append(jsonString(processingVersion)).append(",\n"); - - // System properties - json.append(" \"system\": {\n"); - boolean first = true; - for (String prop : SAFE_PROPERTIES) { - String value = System.getProperty(prop); - if (value != null) { - if (!first) json.append(",\n"); - // Sanitize line separator for JSON - if (prop.equals("line.separator")) { - value = escapeForJson(value); - } - json.append(" \"").append(prop).append("\": ").append(jsonString(value)); - first = false; - } - } - json.append("\n },\n"); - - // Memory information - Runtime runtime = Runtime.getRuntime(); - json.append(" \"memory\": {\n"); - json.append(" \"totalMemory\": ").append(runtime.totalMemory()).append(",\n"); - json.append(" \"freeMemory\": ").append(runtime.freeMemory()).append(",\n"); - json.append(" \"maxMemory\": ").append(runtime.maxMemory()).append(",\n"); - json.append(" \"usedMemory\": ").append(runtime.totalMemory() - runtime.freeMemory()).append("\n"); - json.append(" },\n"); - - // Sketch-specific information - if (applet != null) { - json.append(" \"sketch\": {\n"); - - // Renderer information - PGraphics graphics = applet.g; - if (graphics != null) { - json.append(" \"renderer\": ").append(jsonString(graphics.getClass().getSimpleName())).append(",\n"); - json.append(" \"width\": ").append(applet.width).append(",\n"); - json.append(" \"height\": ").append(applet.height).append(",\n"); - json.append(" \"pixelDensity\": ").append(applet.pixelDensity).append(",\n"); - json.append(" \"frameCount\": ").append(applet.frameCount).append(",\n"); - json.append(" \"frameRate\": ").append(String.format("%.2f", applet.frameRate)).append(",\n"); - } - - json.append(" \"focused\": ").append(applet.focused).append("\n"); - json.append(" },\n"); - } - - // Display information - try { - GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); - GraphicsDevice[] devices = ge.getScreenDevices(); - - json.append(" \"displays\": [\n"); - for (int i = 0; i < devices.length; i++) { - if (i > 0) json.append(",\n"); - GraphicsDevice device = devices[i]; - DisplayMode mode = device.getDisplayMode(); - - json.append(" {\n"); - json.append(" \"id\": ").append(i).append(",\n"); - json.append(" \"width\": ").append(mode.getWidth()).append(",\n"); - json.append(" \"height\": ").append(mode.getHeight()).append(",\n"); - json.append(" \"refreshRate\": ").append(mode.getRefreshRate()).append(",\n"); - json.append(" \"bitDepth\": ").append(mode.getBitDepth()).append("\n"); - json.append(" }"); - } - json.append("\n ]\n"); - - } catch (Exception e) { - json.append(" \"displays\": \"Error: ").append(e.getMessage()).append("\"\n"); - } - - json.append("}"); - return json.toString(); - } - - - /** - * Converts a string to JSON-safe quoted string. - */ - private static String jsonString(String value) { - if (value == null) { - return "null"; - } - return "\"" + escapeForJson(value) + "\""; - } - - - /** - * Escapes special characters for JSON. - */ - private static String escapeForJson(String value) { - return value - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); - } - - - /** - * Formats bytes into human-readable format (KB, MB, GB). - */ - public static String formatBytes(long bytes) { - if (bytes < 1024) return bytes + " B"; - int exp = (int) (Math.log(bytes) / Math.log(1024)); - char unit = "KMGT".charAt(exp - 1); - return String.format("%.2f %sB", bytes / Math.pow(1024, exp), unit); - } -} diff --git a/java/src/processing/mode/java/debug/Debugger.java b/java/src/processing/mode/java/debug/Debugger.java index 1c217f2f2b..e9d42895be 100644 --- a/java/src/processing/mode/java/debug/Debugger.java +++ b/java/src/processing/mode/java/debug/Debugger.java @@ -174,16 +174,6 @@ public void actionPerformed(ActionEvent e) { debugMenu.addSeparator(); - item = Toolkit.newJMenuItemExt("menu.debug.gather_diagnostics"); - item.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - Messages.log("Invoked 'Gather Diagnostics' menu item"); - gatherDiagnostics(); - } - }); - debugMenu.add(item); - item.setEnabled(false); - item = Toolkit.newJMenuItem(Language.text("menu.debug.toggle_breakpoint"), 'B'); item.addActionListener(new ActionListener() { @@ -586,100 +576,6 @@ private static void appendField(StringBuilder sb, ObjectReference obj, ClassType } - /** - * Gather system diagnostics from the running sketch. - *

- * Calls the getDiagnostics() method on the sketch's PApplet instance - * via JDWP and displays the results in a dialog. - *

- */ - public synchronized void gatherDiagnostics() { - if (!isStarted()) { - editor.statusNotice("Sketch must be running in debug mode to gather diagnostics."); - return; - } - - if (!isPaused()) { - editor.statusNotice("Please pause the sketch first (at a breakpoint or use Continue/Step)."); - return; - } - - try { - if (currentThread == null || currentThread.frameCount() == 0) { - editor.statusError("No valid stack frame available."); - return; - } - - ObjectReference appletRef = currentThread.frame(0).thisObject(); - if (appletRef == null) { - editor.statusError("Could not find PApplet instance. This may happen if the sketch " + - "is paused in a static method. Try pausing in an instance method instead."); - return; - } - - ReferenceType appletType = appletRef.referenceType(); - if (!(appletType instanceof ClassType)) { - editor.statusError("PApplet reference is not a ClassType."); - return; - } - - // Walk the class hierarchy to find the getDiagnostics method - Method diagnosticsMethod = null; - ClassType currentClass = (ClassType) appletType; - while (currentClass != null && diagnosticsMethod == null) { - diagnosticsMethod = currentClass.concreteMethodByName("getDiagnostics", "()Ljava/lang/String;"); - if (diagnosticsMethod == null) { - currentClass = currentClass.superclass(); - } - } - - if (diagnosticsMethod == null) { - editor.statusError("getDiagnostics() method not found. Make sure core library is up to date."); - return; - } - - editor.statusNotice("Gathering diagnostics from sketch..."); - Value resultValue = appletRef.invokeMethod( - currentThread, - diagnosticsMethod, - new ArrayList<>(), - ObjectReference.INVOKE_SINGLE_THREADED - ); - - if (resultValue == null) { - editor.statusError("getDiagnostics() returned null."); - return; - } - - if (!(resultValue instanceof StringReference)) { - editor.statusError("getDiagnostics() returned unexpected type: " + resultValue.getClass().getName()); - return; - } - - StringReference result = (StringReference) resultValue; - String diagnosticData = result.value(); - - javax.swing.SwingUtilities.invokeLater(new Runnable() { - public void run() { - DiagnosticsDialog dialog = new DiagnosticsDialog(editor, diagnosticData); - dialog.setVisible(true); - } - }); - - editor.statusNotice("Diagnostics gathered successfully."); - - } catch (IncompatibleThreadStateException e) { - editor.statusError("Thread is not suspended."); - logitse(e); - } catch (ClassCastException e) { - editor.statusError("Unexpected return type from getDiagnostics()."); - e.printStackTrace(); - } catch (Exception e) { - editor.statusError("Error gathering diagnostics: " + e.getMessage()); - e.printStackTrace(); - } - } - // /** Print the current stack trace. */ // public synchronized void printStackTrace() { diff --git a/java/src/processing/mode/java/debug/DiagnosticsDialog.java b/java/src/processing/mode/java/debug/DiagnosticsDialog.java deleted file mode 100644 index 0da087f4a0..0000000000 --- a/java/src/processing/mode/java/debug/DiagnosticsDialog.java +++ /dev/null @@ -1,217 +0,0 @@ -/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ - -/* - Part of the Processing project - http://processing.org - Copyright (c) 2012-25 The Processing Foundation - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License version 2 - as published by the Free Software Foundation. - - 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, write to the Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -*/ - -package processing.mode.java.debug; - -import java.awt.*; -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.StringSelection; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Set; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; - -import processing.app.Language; -import processing.mode.java.JavaEditor; - - -/** - * Dialog for displaying system diagnostics gathered from a running sketch. - *

- * Provides a formatted view of diagnostic information including system - * properties, memory statistics, sketch details, and display information. - * Users can copy the diagnostics to clipboard or export to a text file. - *

- * - * @author Processing Foundation - * @since 4.4 - */ -public class DiagnosticsDialog extends JDialog { - private JTextArea textArea; - private String diagnosticData; - private JavaEditor editor; - - - public DiagnosticsDialog(JavaEditor editor, String diagnosticData) { - super(editor, Language.text("menu.debug.gather_diagnostics"), false); - this.editor = editor; - this.diagnosticData = diagnosticData; - - setLayout(new BorderLayout()); - - textArea = new JTextArea(); - textArea.setEditable(false); - textArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); - textArea.setText(formatDiagnostics(diagnosticData)); - textArea.setCaretPosition(0); - - JScrollPane scrollPane = new JScrollPane(textArea); - scrollPane.setPreferredSize(new Dimension(600, 500)); - add(scrollPane, BorderLayout.CENTER); - - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - buttonPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - JButton copyButton = new JButton(Language.text("menu.edit.copy")); - copyButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - copyToClipboard(); - } - }); - buttonPanel.add(copyButton); - - JButton exportButton = new JButton(Language.text("prompt.export")); - exportButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - exportToFile(); - } - }); - buttonPanel.add(exportButton); - - JButton closeButton = new JButton(Language.text("menu.file.close")); - closeButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - dispose(); - } - }); - buttonPanel.add(closeButton); - - add(buttonPanel, BorderLayout.SOUTH); - - pack(); - setLocationRelativeTo(editor); - } - - - /** - * Formats the JSON diagnostic data into a human-readable format. - */ - private String formatDiagnostics(String jsonData) { - if (jsonData == null || jsonData.isEmpty()) { - return "No diagnostic data available."; - } - - StringBuilder formatted = new StringBuilder(); - formatted.append("System Diagnostics\n"); - formatted.append("==================\n\n"); - - String[] lines = jsonData.split("\n"); - int indentLevel = 0; - Set skipTokens = Set.of("{", "}", "[", "]", "},", "],"); - - for (String line : lines) { - String trimmed = line.trim(); - - if (trimmed.startsWith("}") || trimmed.startsWith("]")) { - indentLevel--; - } - - if (trimmed.length() > 0 && !skipTokens.contains(trimmed)) { - - String display = trimmed.replaceAll("\"", "") - .replaceAll(",", "") - .replaceAll(":", ": "); - - for (int i = 0; i < indentLevel; i++) { - formatted.append(" "); - } - - formatted.append(display).append("\n"); - } - - if (trimmed.startsWith("{") || trimmed.startsWith("[")) { - if (trimmed.contains(":")) { - String sectionName = trimmed.substring(0, trimmed.indexOf(":")); - sectionName = sectionName.replaceAll("\"", ""); - formatted.append("\n").append(sectionName.toUpperCase()).append(":\n"); - } - indentLevel++; - } - } - - formatted.append("\n"); - formatted.append("==================\n"); - formatted.append("Generated: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("\n"); - - return formatted.toString(); - } - - - /** - * Copies diagnostic data to system clipboard. - */ - private void copyToClipboard() { - try { - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - StringSelection selection = new StringSelection(textArea.getText()); - clipboard.setContents(selection, null); - - editor.statusNotice("Diagnostics copied to clipboard."); - } catch (Exception e) { - JOptionPane.showMessageDialog(this, - "Could not copy diagnostics to clipboard.\n" + e.getMessage(), - "Copy Failed", - JOptionPane.WARNING_MESSAGE); - } - } - - - /** - * Exports diagnostic data to a text file. - */ - private void exportToFile() { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setDialogTitle("Save Diagnostics"); - - // Default filename with timestamp - String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - fileChooser.setSelectedFile(new File("diagnostics_" + timestamp + ".txt")); - - int result = fileChooser.showSaveDialog(this); - if (result == JFileChooser.APPROVE_OPTION) { - File file = fileChooser.getSelectedFile(); - - try (FileWriter writer = new FileWriter(file)) { - writer.write(textArea.getText()); - writer.write("\n\n"); - writer.write("Raw JSON Data:\n"); - writer.write("==============\n"); - writer.write(diagnosticData); - - editor.statusNotice("Diagnostics exported to " + file.getName()); - } catch (IOException e) { - JOptionPane.showMessageDialog(this, - "Could not export diagnostics to file.\n" + e.getMessage(), - "Export Failed", - JOptionPane.WARNING_MESSAGE); - } - } - } -}