DebuggerSupport.java revision 953:221a84ef44c0
1/*
2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General  License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General  License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General  License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.internal.runtime;
27
28import static jdk.nashorn.internal.codegen.CompilerConstants.SOURCE;
29import static jdk.nashorn.internal.runtime.UnwarrantedOptimismException.INVALID_PROGRAM_POINT;
30
31import java.lang.invoke.MethodHandle;
32import java.lang.reflect.Field;
33import java.net.URL;
34import java.util.HashSet;
35import java.util.Set;
36import jdk.nashorn.internal.scripts.JS;
37
38/**
39 * This class provides support for external debuggers.  Its primary purpose is
40 * is to simplify the debugger tasks and provide better performance.
41 * Even though the methods are not public, there are still part of the
42 * external debugger interface.
43 */
44final class DebuggerSupport {
45    /**
46     * Hook to force the loading of the DebuggerSupport class so that it is
47     * available to external debuggers.
48     */
49    static boolean FORCELOAD = true;
50
51    static {
52        /**
53         * Hook to force the loading of the DebuggerValueDesc class so that it is
54         * available to external debuggers.
55         */
56        @SuppressWarnings("unused")
57        final
58        DebuggerValueDesc forceLoad = new DebuggerValueDesc(null, false, null, null);
59
60        // Hook to force the loading of the SourceInfo class
61        @SuppressWarnings("unused")
62        final
63        SourceInfo srcInfo = new SourceInfo(null, 0, null, null);
64    }
65
66    /** This class is used to send a bulk description of a value. */
67    static class DebuggerValueDesc {
68        /** Property key (or index) or field name. */
69        final String key;
70
71        /** If the value is expandable. */
72        final boolean expandable;
73
74        /** Property or field value as object. */
75        final Object valueAsObject;
76
77        /** Property or field value as string. */
78        final String valueAsString;
79
80        DebuggerValueDesc(final String key, final boolean expandable, final Object valueAsObject, final String valueAsString) {
81            this.key           = key;
82            this.expandable    = expandable;
83            this.valueAsObject = valueAsObject;
84            this.valueAsString = valueAsString;
85        }
86    }
87
88    static class SourceInfo {
89        final String name;
90        final URL    url;
91        final int    hash;
92        final char[] content;
93
94        SourceInfo(final String name, final int hash, final URL url, final char[] content) {
95            this.name    = name;
96            this.hash    = hash;
97            this.url     = url;
98            this.content = content;
99        }
100    }
101
102    /**
103     * Hook that is called just before invoking method handle
104     * from ScriptFunctionData via invoke, constructor method calls.
105     *
106     * @param mh script class method about to be invoked.
107     */
108    static void notifyInvoke(final MethodHandle mh) {
109        // Do nothing here. This is placeholder method on which a
110        // debugger can place a breakpoint so that it can access the
111        // (script class) method handle that is about to be invoked.
112        // See ScriptFunctionData.invoke and ScriptFunctionData.construct.
113    }
114
115    /**
116     * Return the script source info for the given script class.
117     *
118     * @param clazz compiled script class
119     * @return SourceInfo
120     */
121    static SourceInfo getSourceInfo(final Class<?> clazz) {
122        if (JS.class.isAssignableFrom(clazz)) {
123            try {
124                final Field sourceField = clazz.getDeclaredField(SOURCE.symbolName());
125                sourceField.setAccessible(true);
126                final Source src = (Source) sourceField.get(null);
127                return src.getSourceInfo();
128            } catch (final IllegalAccessException | NoSuchFieldException ignored) {
129                return null;
130            }
131        }
132
133        return null;
134    }
135
136    /**
137     * Return the current context global.
138     * @return context global.
139     */
140    static Object getGlobal() {
141        return Context.getGlobal();
142    }
143
144    /**
145     * Call eval on the current global.
146     * @param scope           Scope to use.
147     * @param self            Receiver to use.
148     * @param string          String to evaluate.
149     * @param returnException true if exceptions are to be returned.
150     * @return Result of eval, or, an exception or null depending on returnException.
151     */
152    static Object eval(final ScriptObject scope, final Object self, final String string, final boolean returnException) {
153        final ScriptObject global = Context.getGlobal();
154        final ScriptObject initialScope = scope != null ? scope : global;
155        final Object callThis = self != null ? self : global;
156        final Context context = global.getContext();
157
158        try {
159            return context.eval(initialScope, string, callThis, ScriptRuntime.UNDEFINED, false);
160        } catch (final Throwable ex) {
161            return returnException ? ex : null;
162        }
163    }
164
165    /**
166     * This method returns a bulk description of an object's properties.
167     * @param object Script object to be displayed by the debugger.
168     * @param all    true if to include non-enumerable values.
169     * @return An array of DebuggerValueDesc.
170     */
171    static DebuggerValueDesc[] valueInfos(final Object object, final boolean all) {
172        assert object instanceof ScriptObject;
173        return getDebuggerValueDescs((ScriptObject)object, all, new HashSet<>());
174    }
175
176    /**
177     * This method returns a debugger description of the value.
178     * @param name  Name of value (property name).
179     * @param value Data value.
180     * @param all   true if to include non-enumerable values.
181     * @return A DebuggerValueDesc.
182     */
183    static DebuggerValueDesc valueInfo(final String name, final Object value, final boolean all) {
184        return valueInfo(name, value, all, new HashSet<>());
185    }
186
187   /**
188     * This method returns a debugger description of the value.
189     * @param name       Name of value (property name).
190     * @param value      Data value.
191     * @param all        true if to include non-enumerable values.
192     * @param duplicates Duplication set to avoid cycles.
193     * @return A DebuggerValueDesc.
194     */
195    private static DebuggerValueDesc valueInfo(final String name, final Object value, final boolean all, final Set<Object> duplicates) {
196        if (value instanceof ScriptObject && !(value instanceof ScriptFunction)) {
197            final ScriptObject object = (ScriptObject)value;
198            return new DebuggerValueDesc(name, !object.isEmpty(), value, objectAsString(object, all, duplicates));
199        }
200        return new DebuggerValueDesc(name, false, value, valueAsString(value));
201    }
202
203    /**
204     * Generate the descriptions for an object's properties.
205     * @param object     Object to introspect.
206     * @param all        true if to include non-enumerable values.
207     * @param duplicates Duplication set to avoid cycles.
208     * @return An array of DebuggerValueDesc.
209     */
210    private static DebuggerValueDesc[] getDebuggerValueDescs(final ScriptObject object, final boolean all, final Set<Object> duplicates) {
211        if (duplicates.contains(object)) {
212            return null;
213        }
214
215        duplicates.add(object);
216
217        final String[] keys = object.getOwnKeys(all);
218        final DebuggerValueDesc[] descs = new DebuggerValueDesc[keys.length];
219
220        for (int i = 0; i < keys.length; i++) {
221            final String key = keys[i];
222            descs[i] = valueInfo(key, object.get(key), all, duplicates);
223        }
224
225        duplicates.remove(object);
226
227        return descs;
228    }
229
230    /**
231     * Generate a string representation of a Script object.
232     * @param object     Script object to represent.
233     * @param all        true if to include non-enumerable values.
234     * @param duplicates Duplication set to avoid cycles.
235     * @return String representation.
236     */
237    private static String objectAsString(final ScriptObject object, final boolean all, final Set<Object> duplicates) {
238        final StringBuilder sb = new StringBuilder();
239
240        if (ScriptObject.isArray(object)) {
241            sb.append('[');
242            final long length = object.getLong("length", INVALID_PROGRAM_POINT);
243
244            for (long i = 0; i < length; i++) {
245                if (object.has(i)) {
246                    final Object valueAsObject = object.get(i);
247                    final boolean isUndefined = valueAsObject == ScriptRuntime.UNDEFINED;
248
249                    if (isUndefined) {
250                        if (i != 0) {
251                            sb.append(",");
252                        }
253                    } else {
254                        if (i != 0) {
255                            sb.append(", ");
256                        }
257
258                        if (valueAsObject instanceof ScriptObject && !(valueAsObject instanceof ScriptFunction)) {
259                            final String objectString = objectAsString((ScriptObject)valueAsObject, all, duplicates);
260                            sb.append(objectString != null ? objectString : "{...}");
261                        } else {
262                            sb.append(valueAsString(valueAsObject));
263                        }
264                    }
265                } else {
266                    if (i != 0) {
267                        sb.append(',');
268                    }
269                }
270            }
271
272            sb.append(']');
273        } else {
274            sb.append('{');
275            final DebuggerValueDesc[] descs = getDebuggerValueDescs(object, all, duplicates);
276
277            if (descs != null) {
278                for (int i = 0; i < descs.length; i++) {
279                    if (i != 0) {
280                        sb.append(", ");
281                    }
282
283                    final String valueAsString = descs[i].valueAsString;
284                    sb.append(descs[i].key);
285                    sb.append(": ");
286                    sb.append(valueAsString);
287                }
288            }
289
290            sb.append('}');
291        }
292
293        return sb.toString();
294    }
295
296    /**
297     * This method returns a string representation of a value.
298     * @param value Arbitrary value to be displayed by the debugger.
299     * @return A string representation of the value or an array of DebuggerValueDesc.
300     */
301    static String valueAsString(final Object value) {
302        final JSType type = JSType.of(value);
303
304        switch (type) {
305        case BOOLEAN:
306            return value.toString();
307
308        case STRING:
309            return escape(value.toString());
310
311        case NUMBER:
312            return JSType.toString(((Number)value).doubleValue());
313
314        case NULL:
315            return "null";
316
317        case UNDEFINED:
318            return "undefined";
319
320        case OBJECT:
321            return ScriptRuntime.safeToString(value);
322
323        case FUNCTION:
324            if (value instanceof ScriptFunction) {
325                return ((ScriptFunction)value).toSource();
326            }
327            return value.toString();
328
329        default:
330            return value.toString();
331        }
332    }
333
334    /**
335     * Escape a string into a form that can be parsed by JavaScript.
336     * @param value String to be escaped.
337     * @return Escaped string.
338     */
339    private static String escape(final String value) {
340        final StringBuilder sb = new StringBuilder();
341
342        sb.append("\"");
343
344        for (final char ch : value.toCharArray()) {
345            switch (ch) {
346            case '\\':
347                sb.append("\\\\");
348                break;
349            case '"':
350                sb.append("\\\"");
351                break;
352            case '\'':
353                sb.append("\\\'");
354                break;
355            case '\b':
356                sb.append("\\b");
357                break;
358            case '\f':
359                sb.append("\\f");
360                break;
361            case '\n':
362                sb.append("\\n");
363                break;
364            case '\r':
365                sb.append("\\r");
366                break;
367            case '\t':
368                sb.append("\\t");
369                break;
370            default:
371                if (ch < ' ' || ch >= 0xFF) {
372                    sb.append("\\u");
373
374                    final String hex = Integer.toHexString(ch);
375                    for (int i = hex.length(); i < 4; i++) {
376                        sb.append('0');
377                    }
378                    sb.append(hex);
379                } else {
380                    sb.append(ch);
381                }
382
383                break;
384            }
385        }
386
387        sb.append("\"");
388
389        return sb.toString();
390    }
391}
392
393
394