1 /*
2  * Copyright (c) 2015, 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 package jdk.nashorn.tools.jjs;
27 
28 import java.io.IOException;
29 import java.nio.charset.Charset;
30 import java.nio.file.ClosedWatchServiceException;
31 import java.nio.file.FileSystems;
32 import java.nio.file.Files;
33 import java.nio.file.Path;
34 import java.nio.file.WatchKey;
35 import java.nio.file.WatchService;
36 import java.util.List;
37 import java.util.function.Consumer;
38 import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
39 import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
40 import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
41 
42 final class ExternalEditor {
43     private final Consumer<String> errorHandler;
44     private final Consumer<String> saveHandler;
45     private final Console input;
46 
47     private WatchService watcher;
48     private Thread watchedThread;
49     private Path dir;
50     private Path tmpfile;
51 
ExternalEditor(final Consumer<String> errorHandler, final Consumer<String> saveHandler, final Console input)52     ExternalEditor(final Consumer<String> errorHandler, final Consumer<String> saveHandler, final Console input) {
53         this.errorHandler = errorHandler;
54         this.saveHandler = saveHandler;
55         this.input = input;
56     }
57 
edit(final String cmd, final String initialText)58     private void edit(final String cmd, final String initialText) {
59         try {
60             setupWatch(initialText);
61             launch(cmd);
62         } catch (IOException ex) {
63             errorHandler.accept(ex.getMessage());
64         }
65     }
66 
67     /**
68      * Creates a WatchService and registers the given directory
69      */
setupWatch(final String initialText)70     private void setupWatch(final String initialText) throws IOException {
71         this.watcher = FileSystems.getDefault().newWatchService();
72         this.dir = Files.createTempDirectory("REPL");
73         this.tmpfile = Files.createTempFile(dir, null, ".js");
74         Files.write(tmpfile, initialText.getBytes(Charset.forName("UTF-8")));
75         dir.register(watcher,
76                 ENTRY_CREATE,
77                 ENTRY_DELETE,
78                 ENTRY_MODIFY);
79         watchedThread = new Thread(() -> {
80             for (;;) {
81                 WatchKey key;
82                 try {
83                     key = watcher.take();
84                 } catch (final ClosedWatchServiceException ex) {
85                     break;
86                 } catch (final InterruptedException ex) {
87                     continue; // tolerate an intrupt
88                 }
89 
90                 if (!key.pollEvents().isEmpty()) {
91                     if (!input.terminalEditorRunning()) {
92                         saveFile();
93                     }
94                 }
95 
96                 boolean valid = key.reset();
97                 if (!valid) {
98                     errorHandler.accept("Invalid key");
99                     break;
100                 }
101             }
102         });
103         watchedThread.start();
104     }
105 
launch(final String cmd)106     private void launch(final String cmd) throws IOException {
107         ProcessBuilder pb = new ProcessBuilder(cmd, tmpfile.toString());
108         pb = pb.inheritIO();
109 
110         try {
111             input.suspend();
112             Process process = pb.start();
113             process.waitFor();
114         } catch (final IOException ex) {
115             errorHandler.accept("process IO failure: " + ex.getMessage());
116         } catch (final InterruptedException ex) {
117             errorHandler.accept("process interrupt: " + ex.getMessage());
118         } finally {
119             try {
120                 watcher.close();
121                 watchedThread.join(); //so that saveFile() is finished.
122                 saveFile();
123             } catch (InterruptedException ex) {
124                 errorHandler.accept("process interrupt: " + ex.getMessage());
125             } finally {
126                 input.resume();
127             }
128         }
129     }
130 
saveFile()131     private void saveFile() {
132         List<String> lines;
133         try {
134             lines = Files.readAllLines(tmpfile);
135         } catch (final IOException ex) {
136             errorHandler.accept("Failure read edit file: " + ex.getMessage());
137             return ;
138         }
139         StringBuilder sb = new StringBuilder();
140         for (String ln : lines) {
141             sb.append(ln);
142             sb.append('\n');
143         }
144         saveHandler.accept(sb.toString());
145     }
146 
edit(final String cmd, final Consumer<String> errorHandler, final String initialText, final Consumer<String> saveHandler, final Console input)147     static void edit(final String cmd, final Consumer<String> errorHandler, final String initialText,
148             final Consumer<String> saveHandler, final Console input) {
149         ExternalEditor ed = new ExternalEditor(errorHandler,  saveHandler, input);
150         ed.edit(cmd, initialText);
151     }
152 }
153