1/*
2 * Copyright (c) 1998, 2012, 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 Public 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 Public 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 Public 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
26/*
27 * This source code is provided to illustrate the usage of a given feature
28 * or technique and has been deliberately simplified. Additional steps
29 * required for a production-quality application, such as security checks,
30 * input validation and proper error handling, might not be present in
31 * this sample code.
32 */
33
34
35package com.sun.tools.example.debug.tty;
36
37import com.sun.jdi.*;
38import com.sun.jdi.connect.*;
39import com.sun.jdi.request.EventRequestManager;
40import com.sun.jdi.request.ThreadStartRequest;
41import com.sun.jdi.request.ThreadDeathRequest;
42
43import java.util.*;
44import java.util.regex.*;
45import java.io.*;
46
47class VMConnection {
48
49    private VirtualMachine vm;
50    private Process process = null;
51    private int outputCompleteCount = 0;
52
53    private final Connector connector;
54    private final Map<String, com.sun.jdi.connect.Connector.Argument> connectorArgs;
55    private final int traceFlags;
56
57    synchronized void notifyOutputComplete() {
58        outputCompleteCount++;
59        notifyAll();
60    }
61
62    synchronized void waitOutputComplete() {
63        // Wait for stderr and stdout
64        if (process != null) {
65            while (outputCompleteCount < 2) {
66                try {wait();} catch (InterruptedException e) {}
67            }
68        }
69    }
70
71    private Connector findConnector(String name) {
72        for (Connector connector :
73                 Bootstrap.virtualMachineManager().allConnectors()) {
74            if (connector.name().equals(name)) {
75                return connector;
76            }
77        }
78        return null;
79    }
80
81    private Map <String, com.sun.jdi.connect.Connector.Argument> parseConnectorArgs(Connector connector, String argString) {
82        Map<String, com.sun.jdi.connect.Connector.Argument> arguments = connector.defaultArguments();
83
84        /*
85         * We are parsing strings of the form:
86         *    name1=value1,[name2=value2,...]
87         * However, the value1...valuen substrings may contain
88         * embedded comma(s), so make provision for quoting inside
89         * the value substrings. (Bug ID 4285874)
90         */
91        String regexPattern =
92            "(quote=[^,]+,)|" +           // special case for quote=.,
93            "(\\w+=)" +                   // name=
94            "(((\"[^\"]*\")|" +           //   ( "l , ue"
95            "('[^']*')|" +                //     'l , ue'
96            "([^,'\"]+))+,)";             //     v a l u e )+ ,
97        Pattern p = Pattern.compile(regexPattern);
98        Matcher m = p.matcher(argString);
99        while (m.find()) {
100            int startPosition = m.start();
101            int endPosition = m.end();
102            if (startPosition > 0) {
103                /*
104                 * It is an error if parsing skips over any part of argString.
105                 */
106                throw new IllegalArgumentException
107                    (MessageOutput.format("Illegal connector argument",
108                                          argString));
109            }
110
111            String token = argString.substring(startPosition, endPosition);
112            int index = token.indexOf('=');
113            String name = token.substring(0, index);
114            String value = token.substring(index + 1,
115                                           token.length() - 1); // Remove comma delimiter
116
117            /*
118             * for values enclosed in quotes (single and/or double quotes)
119             * strip off enclosing quote chars
120             * needed for quote enclosed delimited substrings
121             */
122            if (name.equals("options")) {
123                StringBuilder sb = new StringBuilder();
124                for (String s : splitStringAtNonEnclosedWhiteSpace(value)) {
125                    while (isEnclosed(s, "\"") || isEnclosed(s, "'")) {
126                        s = s.substring(1, s.length() - 1);
127                    }
128                    sb.append(s);
129                    sb.append(" ");
130                }
131                value = sb.toString();
132            }
133
134            Connector.Argument argument = arguments.get(name);
135            if (argument == null) {
136                throw new IllegalArgumentException
137                    (MessageOutput.format("Argument is not defined for connector:",
138                                          new Object [] {name, connector.name()}));
139            }
140            argument.setValue(value);
141
142            argString = argString.substring(endPosition); // Remove what was just parsed...
143            m = p.matcher(argString);                     //    and parse again on what is left.
144        }
145        if ((! argString.equals(",")) && (argString.length() > 0)) {
146            /*
147             * It is an error if any part of argString is left over,
148             * unless it was empty to begin with.
149             */
150            throw new IllegalArgumentException
151                (MessageOutput.format("Illegal connector argument", argString));
152        }
153        return arguments;
154    }
155
156    private static boolean isEnclosed(String value, String enclosingChar) {
157        if (value.indexOf(enclosingChar) == 0) {
158            int lastIndex = value.lastIndexOf(enclosingChar);
159            if (lastIndex > 0 && lastIndex  == value.length() - 1) {
160                return true;
161            }
162        }
163        return false;
164    }
165
166    private static List<String> splitStringAtNonEnclosedWhiteSpace(String value) throws IllegalArgumentException {
167        List<String> al = new ArrayList<String>();
168        char[] arr;
169        int startPosition = 0;
170        int endPosition = 0;
171        final char SPACE = ' ';
172        final char DOUBLEQ = '"';
173        final char SINGLEQ = '\'';
174
175        /*
176         * An "open" or "active" enclosing state is where
177         * the first valid start quote qualifier is found,
178         * and there is a search in progress for the
179         * relevant end matching quote
180         *
181         * enclosingTargetChar set to SPACE
182         * is used to signal a non open enclosing state
183         */
184        char enclosingTargetChar = SPACE;
185
186        if (value == null) {
187            throw new IllegalArgumentException
188                (MessageOutput.format("value string is null"));
189        }
190
191        // split parameter string into individual chars
192        arr = value.toCharArray();
193
194        for (int i = 0; i < arr.length; i++) {
195            switch (arr[i]) {
196                case SPACE: {
197                    // do nothing for spaces
198                    // unless last in array
199                    if (isLastChar(arr, i)) {
200                        endPosition = i;
201                        // break for substring creation
202                        break;
203                    }
204                    continue;
205                }
206                case DOUBLEQ:
207                case SINGLEQ: {
208                    if (enclosingTargetChar == arr[i]) {
209                        // potential match to close open enclosing
210                        if (isNextCharWhitespace(arr, i)) {
211                            // if peek next is whitespace
212                            // then enclosing is a valid substring
213                            endPosition = i;
214                            // reset enclosing target char
215                            enclosingTargetChar = SPACE;
216                            // break for substring creation
217                            break;
218                        }
219                    }
220                    if (enclosingTargetChar == SPACE) {
221                        // no open enclosing state
222                        // handle as normal char
223                        if (isPreviousCharWhitespace(arr, i)) {
224                            startPosition = i;
225                            // peek forward for end candidates
226                            if (value.indexOf(arr[i], i + 1) >= 0) {
227                                // set open enclosing state by
228                                // setting up the target char
229                                enclosingTargetChar = arr[i];
230                            } else {
231                                // no more target chars left to match
232                                // end enclosing, handle as normal char
233                                if (isNextCharWhitespace(arr, i)) {
234                                    endPosition = i;
235                                    // break for substring creation
236                                    break;
237                                }
238                            }
239                        }
240                    }
241                    continue;
242                }
243                default: {
244                    // normal non-space, non-" and non-' chars
245                    if (enclosingTargetChar == SPACE) {
246                        // no open enclosing state
247                        if (isPreviousCharWhitespace(arr, i)) {
248                            // start of space delim substring
249                            startPosition = i;
250                        }
251                        if (isNextCharWhitespace(arr, i)) {
252                            // end of space delim substring
253                            endPosition = i;
254                            // break for substring creation
255                            break;
256                        }
257                    }
258                    continue;
259                }
260            }
261
262            // break's end up here
263            if (startPosition > endPosition) {
264                throw new IllegalArgumentException
265                    (MessageOutput.format("Illegal option values"));
266            }
267
268            // extract substring and add to List<String>
269            al.add(value.substring(startPosition, ++endPosition));
270
271            // set new start position
272            i = startPosition = endPosition;
273
274        } // for loop
275
276        return al;
277    }
278
279    static private boolean isPreviousCharWhitespace(char[] arr, int curr_pos) {
280        return isCharWhitespace(arr, curr_pos - 1);
281    }
282
283    static private boolean isNextCharWhitespace(char[] arr, int curr_pos) {
284        return isCharWhitespace(arr, curr_pos + 1);
285    }
286
287    static private boolean isCharWhitespace(char[] arr, int pos) {
288        if (pos < 0 || pos >= arr.length) {
289            // outside arraybounds is considered an implicit space
290            return true;
291        }
292        if (arr[pos] == ' ') {
293            return true;
294        }
295        return false;
296    }
297
298    static private boolean isLastChar(char[] arr, int pos) {
299        return (pos + 1 == arr.length);
300    }
301
302    VMConnection(String connectSpec, int traceFlags) {
303        String nameString;
304        String argString;
305        int index = connectSpec.indexOf(':');
306        if (index == -1) {
307            nameString = connectSpec;
308            argString = "";
309        } else {
310            nameString = connectSpec.substring(0, index);
311            argString = connectSpec.substring(index + 1);
312        }
313
314        connector = findConnector(nameString);
315        if (connector == null) {
316            throw new IllegalArgumentException
317                (MessageOutput.format("No connector named:", nameString));
318        }
319
320        connectorArgs = parseConnectorArgs(connector, argString);
321        this.traceFlags = traceFlags;
322    }
323
324    synchronized VirtualMachine open() {
325        if (connector instanceof LaunchingConnector) {
326            vm = launchTarget();
327        } else if (connector instanceof AttachingConnector) {
328            vm = attachTarget();
329        } else if (connector instanceof ListeningConnector) {
330            vm = listenTarget();
331        } else {
332            throw new InternalError
333                (MessageOutput.format("Invalid connect type"));
334        }
335        vm.setDebugTraceMode(traceFlags);
336        if (vm.canBeModified()){
337            setEventRequests(vm);
338            resolveEventRequests();
339        }
340        /*
341         * Now that the vm connection is open, fetch the debugee
342         * classpath and set up a default sourcepath.
343         * (Unless user supplied a sourcepath on the command line)
344         * (Bug ID 4186582)
345         */
346        if (Env.getSourcePath().length() == 0) {
347            if (vm instanceof PathSearchingVirtualMachine) {
348                PathSearchingVirtualMachine psvm =
349                    (PathSearchingVirtualMachine) vm;
350                Env.setSourcePath(psvm.classPath());
351            } else {
352                Env.setSourcePath(".");
353            }
354        }
355
356        return vm;
357    }
358
359    boolean setConnectorArg(String name, String value) {
360        /*
361         * Too late if the connection already made
362         */
363        if (vm != null) {
364            return false;
365        }
366
367        Connector.Argument argument = connectorArgs.get(name);
368        if (argument == null) {
369            return false;
370        }
371        argument.setValue(value);
372        return true;
373    }
374
375    String connectorArg(String name) {
376        Connector.Argument argument = connectorArgs.get(name);
377        if (argument == null) {
378            return "";
379        }
380        return argument.value();
381    }
382
383    public synchronized VirtualMachine vm() {
384        if (vm == null) {
385            throw new VMNotConnectedException();
386        } else {
387            return vm;
388        }
389    }
390
391    boolean isOpen() {
392        return (vm != null);
393    }
394
395    boolean isLaunch() {
396        return (connector instanceof LaunchingConnector);
397    }
398
399    public void disposeVM() {
400        try {
401            if (vm != null) {
402                vm.dispose();
403                vm = null;
404            }
405        } finally {
406            if (process != null) {
407                process.destroy();
408                process = null;
409            }
410            waitOutputComplete();
411        }
412    }
413
414    private void setEventRequests(VirtualMachine vm) {
415        EventRequestManager erm = vm.eventRequestManager();
416
417        // Normally, we want all uncaught exceptions.  We request them
418        // via the same mechanism as Commands.commandCatchException()
419        // so the user can ignore them later if they are not
420        // interested.
421        // FIXME: this works but generates spurious messages on stdout
422        //        during startup:
423        //          Set uncaught java.lang.Throwable
424        //          Set deferred uncaught java.lang.Throwable
425        Commands evaluator = new Commands();
426        evaluator.commandCatchException
427            (new StringTokenizer("uncaught java.lang.Throwable"));
428
429        ThreadStartRequest tsr = erm.createThreadStartRequest();
430        tsr.enable();
431        ThreadDeathRequest tdr = erm.createThreadDeathRequest();
432        tdr.enable();
433    }
434
435    private void resolveEventRequests() {
436        Env.specList.resolveAll();
437    }
438
439    private void dumpStream(InputStream stream) throws IOException {
440        BufferedReader in =
441            new BufferedReader(new InputStreamReader(stream));
442        int i;
443        try {
444            while ((i = in.read()) != -1) {
445                   MessageOutput.printDirect((char)i);// Special case: use
446                                                      //   printDirect()
447            }
448        } catch (IOException ex) {
449            String s = ex.getMessage();
450            if (!s.startsWith("Bad file number")) {
451                  throw ex;
452            }
453            // else we got a Bad file number IOException which just means
454            // that the debuggee has gone away.  We'll just treat it the
455            // same as if we got an EOF.
456        }
457    }
458
459    /**
460     *  Create a Thread that will retrieve and display any output.
461     *  Needs to be high priority, else debugger may exit before
462     *  it can be displayed.
463     */
464    private void displayRemoteOutput(final InputStream stream) {
465        Thread thr = new Thread("output reader") {
466            @Override
467            public void run() {
468                try {
469                    dumpStream(stream);
470                } catch (IOException ex) {
471                    MessageOutput.fatalError("Failed reading output");
472                } finally {
473                    notifyOutputComplete();
474                }
475            }
476        };
477        thr.setPriority(Thread.MAX_PRIORITY-1);
478        thr.start();
479    }
480
481    private void dumpFailedLaunchInfo(Process process) {
482        try {
483            dumpStream(process.getErrorStream());
484            dumpStream(process.getInputStream());
485        } catch (IOException e) {
486            MessageOutput.println("Unable to display process output:",
487                                  e.getMessage());
488        }
489    }
490
491    /* launch child target vm */
492    private VirtualMachine launchTarget() {
493        LaunchingConnector launcher = (LaunchingConnector)connector;
494        try {
495            VirtualMachine vm = launcher.launch(connectorArgs);
496            process = vm.process();
497            displayRemoteOutput(process.getErrorStream());
498            displayRemoteOutput(process.getInputStream());
499            return vm;
500        } catch (IOException ioe) {
501            ioe.printStackTrace();
502            MessageOutput.fatalError("Unable to launch target VM.");
503        } catch (IllegalConnectorArgumentsException icae) {
504            icae.printStackTrace();
505            MessageOutput.fatalError("Internal debugger error.");
506        } catch (VMStartException vmse) {
507            MessageOutput.println("vmstartexception", vmse.getMessage());
508            MessageOutput.println();
509            dumpFailedLaunchInfo(vmse.process());
510            MessageOutput.fatalError("Target VM failed to initialize.");
511        }
512        return null; // Shuts up the compiler
513    }
514
515    /* attach to running target vm */
516    private VirtualMachine attachTarget() {
517        AttachingConnector attacher = (AttachingConnector)connector;
518        try {
519            return attacher.attach(connectorArgs);
520        } catch (IOException ioe) {
521            ioe.printStackTrace();
522            MessageOutput.fatalError("Unable to attach to target VM.");
523        } catch (IllegalConnectorArgumentsException icae) {
524            icae.printStackTrace();
525            MessageOutput.fatalError("Internal debugger error.");
526        }
527        return null; // Shuts up the compiler
528    }
529
530    /* listen for connection from target vm */
531    private VirtualMachine listenTarget() {
532        ListeningConnector listener = (ListeningConnector)connector;
533        try {
534            String retAddress = listener.startListening(connectorArgs);
535            MessageOutput.println("Listening at address:", retAddress);
536            vm = listener.accept(connectorArgs);
537            listener.stopListening(connectorArgs);
538            return vm;
539        } catch (IOException ioe) {
540            ioe.printStackTrace();
541            MessageOutput.fatalError("Unable to attach to target VM.");
542        } catch (IllegalConnectorArgumentsException icae) {
543            icae.printStackTrace();
544            MessageOutput.fatalError("Internal debugger error.");
545        }
546        return null; // Shuts up the compiler
547    }
548}
549