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 
  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 
  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      */
  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 
 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 
 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 
 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 }