From 0f95603cfd69341b7819d34f1b1d1827d2323a44 Mon Sep 17 00:00:00 2001 From: Harsh Date: Sun, 19 Oct 2025 16:35:47 +0530 Subject: [PATCH 1/2] 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/2] 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(",", "")