From 7a6b2b221b66a4fab2d2d0b7e12ae863864ac33a Mon Sep 17 00:00:00 2001 From: Ben Fry Date: Sat, 3 Dec 2016 09:20:57 -0500 Subject: [PATCH] splitting Console for better recording --- app/src/processing/app/Console.java | 504 ++++++++++++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 app/src/processing/app/Console.java diff --git a/app/src/processing/app/Console.java b/app/src/processing/app/Console.java new file mode 100644 index 000000000..b5f5b9496 --- /dev/null +++ b/app/src/processing/app/Console.java @@ -0,0 +1,504 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2004-10 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + 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 2 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, write to the Free Software Foundation, + Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +package processing.app.ui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.event.*; +import java.io.*; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.swing.*; +import javax.swing.border.MatteBorder; +import javax.swing.text.*; + +import processing.app.*; +import processing.core.PApplet; + + +/** + * Message console that sits below the editing area. + *

+ * Be careful when debugging this class, because if it's throwing exceptions, + * don't take over System.err, and debug while watching just System.out + * or just call println() or whatever directly to systemOut or systemErr. + *

+ * Also note that encodings will not work properly when run from Eclipse. This + * means that if you use non-ASCII characters in a println() or some such, + * the characters won't print properly in the Processing and/or Eclipse console. + * It seems that Eclipse's console-grabbing and that of Processing don't + * get along with one another. Use 'ant run' to work on encoding-related issues. + */ +public class EditorConsole extends JScrollPane { + Editor editor; + + Timer flushTimer; + + JTextPane consoleTextPane; + BufferedStyledDocument consoleDoc; + + MutableAttributeSet stdStyle; + MutableAttributeSet errStyle; + + int maxLineCount; + + PrintStream sketchOut; + PrintStream sketchErr; + + // Single static instance shared because there's only one real System.out. + // Within the input handlers, the currentConsole variable will be used to + // echo things to the correct location. + + static PrintStream systemOut; + static PrintStream systemErr; + + static PrintStream consoleOut; + static PrintStream consoleErr; + + static OutputStream stdoutFile; + static OutputStream stderrFile; + + static EditorConsole currentConsole; + + // For 0185, moved the first init to this static { } block, so that we never + // have a situation that causes systemOut/Err to not be set properly. + static { + systemOut = System.out; + systemErr = System.err; + + // placing everything inside a try block because this can be a dangerous + // time for the lights to blink out and crash for and obscure reason. + try { + // Create output files that will have a randomized name. Has to + // be randomized otherwise another instance of Processing (or one of its + // sister IDEs) might collide with the file causing permissions problems. + // The files and folders are not deleted on exit because they may be + // needed for debugging or bug reporting. + SimpleDateFormat formatter = new SimpleDateFormat("yyMMdd"); + String randy = PApplet.nf((int) (1000 * Math.random()), 4); + String stamp = formatter.format(new Date()) + "_" + randy; + + File consoleDir = Base.getSettingsFile("console"); + consoleDir.mkdirs(); + File outFile = new File(consoleDir, stamp + ".out"); + stdoutFile = new FileOutputStream(outFile); + File errFile = new File(consoleDir, stamp + ".err"); + stderrFile = new FileOutputStream(errFile); + + consoleOut = new PrintStream(new EditorConsoleStream(false, null)); + consoleErr = new PrintStream(new EditorConsoleStream(true, null)); + + System.setOut(consoleOut); + System.setErr(consoleErr); + +// } catch (Exception e) { +// stdoutFile = null; +// stderrFile = null; +// +// e.printStackTrace(); +// Base.showWarning("Console Error", +// "A problem occurred while trying to open the\n" + +// "files used to store the console output.", e); + } catch (Exception e) { + stdoutFile = null; + stderrFile = null; + + consoleOut = null; + consoleErr = null; + + System.setOut(systemOut); + System.setErr(systemErr); + + e.printStackTrace(systemErr); + } + } + + + public EditorConsole(Editor editor) { + this.editor = editor; + + maxLineCount = Preferences.getInteger("console.length"); + + consoleDoc = new BufferedStyledDocument(10000, maxLineCount); + consoleTextPane = new JTextPane(consoleDoc); + consoleTextPane.setEditable(false); + + updateMode(); + + // add the jtextpane to this scrollpane + this.setViewportView(consoleTextPane); + + sketchOut = new PrintStream(new EditorConsoleStream(false, this)); + sketchErr = new PrintStream(new EditorConsoleStream(true, this)); + + startTimer(); + } + + + protected void flush() { + // only if new text has been added + if (consoleDoc.hasAppendage) { + // insert the text that's been added in the meantime + consoleDoc.insertAll(); + // always move to the end of the text as it's added + consoleTextPane.setCaretPosition(consoleDoc.getLength()); + } + } + + + /** + * Start the timer that handles flushing the console text. Has to be started + * and stopped/cleared because the Timer thread will keep a reference to its + * Editor around even after the Editor has been closed, leaking memory. + */ + protected void startTimer() { + if (flushTimer == null) { + // periodically post buffered messages to the console + // should the interval come from the preferences file? + flushTimer = new Timer(250, new ActionListener() { + public void actionPerformed(ActionEvent evt) { + flush(); + } + }); + flushTimer.start(); + } + } + + + protected void stopTimer() { + if (flushTimer != null) { + flush(); // clear anything that's there + flushTimer.stop(); + flushTimer = null; + } + } + + + public PrintStream getOut() { + return sketchOut; + } + + + public PrintStream getErr() { + return sketchErr; + } + + + /** + * Update the font family and sizes based on the Preferences window. + */ + protected void updateAppearance() { + String fontFamily = Preferences.get("editor.font.family"); + int fontSize = Preferences.getInteger("console.font.size"); + StyleConstants.setFontFamily(stdStyle, fontFamily); + StyleConstants.setFontSize(stdStyle, fontSize); + StyleConstants.setFontFamily(errStyle, fontFamily); + StyleConstants.setFontSize(errStyle, fontSize); + clear(); // otherwise we'll have mixed fonts + } + + + /** + * Change coloring, fonts, etc in response to a mode change. + */ + protected void updateMode() { + Mode mode = editor.getMode(); + + // necessary? + MutableAttributeSet standard = new SimpleAttributeSet(); + StyleConstants.setAlignment(standard, StyleConstants.ALIGN_LEFT); + consoleDoc.setParagraphAttributes(0, 0, standard, true); + + Font font = Preferences.getFont("console.font"); + + // build styles for different types of console output + Color bgColor = mode.getColor("console.color"); + Color fgColorOut = mode.getColor("console.output.color"); + Color fgColorErr = mode.getColor("console.error.color"); + + // Make things line up with the Editor above. If this is ever removed, + // setBorder(null) should be called instead. The defaults are nasty. + setBorder(new MatteBorder(0, Editor.LEFT_GUTTER, 0, 0, bgColor)); + + stdStyle = new SimpleAttributeSet(); + StyleConstants.setForeground(stdStyle, fgColorOut); + StyleConstants.setBackground(stdStyle, bgColor); + StyleConstants.setFontSize(stdStyle, font.getSize()); + StyleConstants.setFontFamily(stdStyle, font.getFamily()); + StyleConstants.setBold(stdStyle, font.isBold()); + StyleConstants.setItalic(stdStyle, font.isItalic()); + + errStyle = new SimpleAttributeSet(); + StyleConstants.setForeground(errStyle, fgColorErr); + StyleConstants.setBackground(errStyle, bgColor); + StyleConstants.setFontSize(errStyle, font.getSize()); + StyleConstants.setFontFamily(errStyle, font.getFamily()); + StyleConstants.setBold(errStyle, font.isBold()); + StyleConstants.setItalic(errStyle, font.isItalic()); + + if (UIManager.getLookAndFeel().getID().equals("Nimbus")) { + getViewport().setBackground(bgColor); + consoleTextPane.setOpaque(false); + consoleTextPane.setBackground(new Color(0, 0, 0, 0)); + } else { + consoleTextPane.setBackground(bgColor); + } + + // calculate height of a line of text in pixels + // and size window accordingly + FontMetrics metrics = this.getFontMetrics(font); + int height = metrics.getAscent() + metrics.getDescent(); + int lines = Preferences.getInteger("console.lines"); //, 4); + int sizeFudge = 6; //10; // unclear why this is necessary, but it is + setPreferredSize(new Dimension(1024, (height * lines) + sizeFudge)); + setMinimumSize(new Dimension(1024, (height * 4) + sizeFudge)); + } + + + static public void setEditor(Editor editor) { + if (currentConsole != null) { + currentConsole.stopTimer(); // allow to be garbage collected + } + currentConsole = editor.console; + currentConsole.startTimer(); + } + + + /** + * Close the streams so that the temporary files can be deleted. + *

+ * File.deleteOnExit() cannot be used because the stdout and stderr + * files are inside a folder, and have to be deleted before the + * folder itself is deleted, which can't be guaranteed when using + * the deleteOnExit() method. + */ + public static void handleQuit() { + // replace original streams to remove references to console's streams + System.setOut(systemOut); + System.setErr(systemErr); + + try { + // close the PrintStream + if (consoleOut != null) consoleOut.close(); + if (consoleErr != null) consoleErr.close(); + + // also have to close the original FileOutputStream + // otherwise it won't be shut down completely + if (stdoutFile != null) stdoutFile.close(); + if (stderrFile != null) stderrFile.close(); + + } catch (IOException e) { + e.printStackTrace(systemErr); + } + } + + + synchronized public void message(String what, boolean err) { + if (err) { + systemErr.print(what); + } else { + systemOut.print(what); + } + + if (err && (what.contains("invalid context 0x0") || (what.contains("invalid drawable")))) { + // Respectfully declining... This is a quirk of more recent releases of + // Java on Mac OS X, but is widely reported as the source of any other + // bug or problem that a user runs into. It may well be a Processing + // bug, but until we know, we're suppressing the messages. + } else if (err && what.contains("Make pbuffer:")) { + // Remove initalization warning from LWJGL. + } else if (err && what.contains("XInitThreads() called for concurrent")) { + // "Info: XInitThreads() called for concurrent Thread support" message on Linux + } else if (!err && what.contains("Listening for transport dt_socket at address")) { + // Message from the JVM about the socket launch for debug + // Listening for transport dt_socket at address: 8727 + } else { + // Append a piece of text to the console. Swing components are NOT + // thread-safe, and since the MessageSiphon instantiates new threads, + // and in those callbacks, they often print output to stdout and stderr, + // which are wrapped by EditorConsoleStream and eventually leads to + // EditorConsole.appendText(), which directly updates the Swing text + // components, causing deadlock. Updates are buffered to the console and + // displayed at regular intervals on Swing's event-dispatching thread. + // (patch by David Mellis) + consoleDoc.appendString(what, err ? errStyle : stdStyle); + } + } + + + public void clear() { + try { + consoleDoc.remove(0, consoleDoc.getLength()); + } catch (BadLocationException e) { + // ignore the error otherwise this will cause an infinite loop + // maybe not a good idea in the long run? + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + private static class EditorConsoleStream extends OutputStream { + final boolean err; // whether stderr or stdout + final byte single[] = new byte[1]; + EditorConsole console; + + public EditorConsoleStream(boolean err, EditorConsole console) { + this.err = err; + this.console = console; + } + + public void close() { } + + public void flush() { } + + public void write(byte b[]) { // appears never to be used + write(b, 0, b.length); + } + + public void write(byte b[], int offset, int length) { + if (console != null) { + console.message(new String(b, offset, length), err); + } else if (currentConsole != null) { + currentConsole.message(new String(b, offset, length), err); + } else { + // If no console is present, still need to write this to the actual + // System.out or System.err. Otherwise we can't !#$!% debug anything. + if (err) { + systemErr.write(b, offset, length); + } else { + systemOut.write(b, offset, length); + } + } + + final OutputStream echo = err ? stderrFile : stdoutFile; + if (echo != null) { + try { + echo.write(b, offset, length); + echo.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void write(int b) { + single[0] = (byte) b; + write(single, 0, 1); + } + } +} + + +// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + +/** + * Buffer updates to the console and output them in batches. For info, see: + * http://java.sun.com/products/jfc/tsc/articles/text/element_buffer and + * http://javatechniques.com/public/java/docs/gui/jtextpane-speed-part2.html + * appendString() is called from multiple threads, and insertAll from the + * swing event thread, so they need to be synchronized + */ +class BufferedStyledDocument extends DefaultStyledDocument { + List elements = new ArrayList(); + int maxLineLength, maxLineCount; + int currentLineLength = 0; + boolean needLineBreak = false; + boolean hasAppendage = false; + + public BufferedStyledDocument(int maxLineLength, int maxLineCount) { + this.maxLineLength = maxLineLength; + this.maxLineCount = maxLineCount; + } + + /** buffer a string for insertion at the end of the DefaultStyledDocument */ + public synchronized void appendString(String str, AttributeSet a) { + // do this so that it's only updated when needed (otherwise console + // updates every 250 ms when an app isn't even running.. see bug 180) + hasAppendage = true; + + // process each line of the string + while (str.length() > 0) { + // newlines within an element have (almost) no effect, so we need to + // replace them with proper paragraph breaks (start and end tags) + if (needLineBreak || currentLineLength > maxLineLength) { + elements.add(new ElementSpec(a, ElementSpec.EndTagType)); + elements.add(new ElementSpec(a, ElementSpec.StartTagType)); + currentLineLength = 0; + } + + if (str.indexOf('\n') == -1) { + elements.add(new ElementSpec(a, ElementSpec.ContentType, + str.toCharArray(), 0, str.length())); + currentLineLength += str.length(); + needLineBreak = false; + str = str.substring(str.length()); // eat the string + } else { + elements.add(new ElementSpec(a, ElementSpec.ContentType, + str.toCharArray(), 0, str.indexOf('\n') + 1)); + needLineBreak = true; + str = str.substring(str.indexOf('\n') + 1); // eat the line + } + } + } + + /** insert the buffered strings */ + public synchronized void insertAll() { + ElementSpec[] elementArray = new ElementSpec[elements.size()]; + elements.toArray(elementArray); + + try { + // check how many lines have been used so far + // if too many, shave off a few lines from the beginning + Element element = super.getDefaultRootElement(); + int lineCount = element.getElementCount(); + int overage = lineCount - maxLineCount; + if (overage > 0) { + // if 1200 lines, and 1000 lines is max, + // find the position of the end of the 200th line + //systemOut.println("overage is " + overage); + Element lineElement = element.getElement(overage); + if (lineElement == null) return; // do nuthin + + int endOffset = lineElement.getEndOffset(); + // remove to the end of the 200th line + super.remove(0, endOffset); + } + super.insert(super.getLength(), elementArray); + + } catch (BadLocationException e) { + // ignore the error otherwise this will cause an infinite loop + // maybe not a good idea in the long run? + } + elements.clear(); + hasAppendage = false; + } +}