1 /*
   2  * Copyright (c) 2016, 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 import jdk.jshell.spi.ExecutionControl;
  27 import jdk.jshell.spi.ExecutionEnv;
  28 import jdk.jshell.spi.SPIResolutionException;
  29 import jdk.jshell.EvalException;
  30 import jdk.jshell.UnresolvedReferenceException;
  31 
  32 import java.io.File;
  33 import java.lang.reflect.Field;
  34 import java.lang.reflect.InvocationTargetException;
  35 import java.lang.reflect.Method;
  36 import java.net.MalformedURLException;
  37 import java.net.URL;
  38 import java.net.URLClassLoader;
  39 import java.security.CodeSource;
  40 import java.util.Arrays;
  41 import java.util.Collection;
  42 import java.util.HashMap;
  43 import java.util.Map;
  44 import java.util.TreeMap;
  45 import java.util.concurrent.atomic.AtomicReference;
  46 
  47 /**
  48  * An implementation of ExecutionControl which executes in the same JVM as the
  49  * JShell core.
  50  *
  51  * @author Grigory Ptashko
  52  */
  53 class LocalExecutionControl implements ExecutionControl {
  54     private class REPLClassLoader extends URLClassLoader {
  55         REPLClassLoader() {
  56             super(new URL[0]);
  57         }
  58 
  59         @Override
  60         protected Class<?> findClass(String name) throws ClassNotFoundException {
  61             debug("findClass %s\n", name);
  62             byte[] b = execEnv.getClassBytes(name);
  63             if (b == null) {
  64                 return super.findClass(name);
  65             }
  66             return super.defineClass(name, b, 0, b.length, (CodeSource)null);
  67         }
  68 
  69         @Override
  70         public void addURL(URL url) {
  71             super.addURL(url);
  72         }
  73     }
  74 
  75     private ExecutionEnv execEnv;
  76     private final Object STOP_LOCK = new Object();
  77     private boolean userCodeRunning = false;
  78     private REPLClassLoader loader = new REPLClassLoader();
  79     private final Map<String, Class<?>> klasses = new TreeMap<>();
  80     private final Map<String, byte[]> classBytes = new HashMap<>();
  81     private ThreadGroup execThreadGroup;
  82 
  83     @Override
  84     public void start(ExecutionEnv execEnv) throws Exception {
  85         this.execEnv = execEnv;
  86 
  87         debug("Process-local code snippets execution control started");
  88     }
  89 
  90     @Override
  91     public void close() {
  92     }
  93 
  94     @Override
  95     public boolean load(Collection<String> classes) {
  96         try {
  97             loadLocal(classes);
  98 
  99             return true;
 100         } catch (ClassNotFoundException | ClassCastException ex) {
 101             debug(ex, "Exception on load operation");
 102         }
 103 
 104         return false;
 105     }
 106 
 107     @Override
 108     public String invoke(String classname, String methodname) throws EvalException, UnresolvedReferenceException {
 109         try {
 110             synchronized (STOP_LOCK) {
 111                 userCodeRunning = true;
 112             }
 113 
 114             // Invoke executable entry point in loaded code
 115             Class<?> klass = klasses.get(classname);
 116             if (klass == null) {
 117                 debug("Invoke failure: no such class loaded %s\n", classname);
 118 
 119                 return "";
 120             }
 121 
 122             Method doitMethod;
 123             try {
 124                 this.getClass().getModule().addReads(klass.getModule());
 125                 this.getClass().getModule().addExports(SPIResolutionException.class.getPackage()
 126                         .getName(), klass.getModule());
 127                 doitMethod = klass.getDeclaredMethod(methodname, new Class<?>[0]);
 128                 doitMethod.setAccessible(true);
 129 
 130                 execThreadGroup = new ThreadGroup("JShell process local execution");
 131 
 132                 AtomicReference<InvocationTargetException> iteEx = new AtomicReference<>();
 133                 AtomicReference<IllegalAccessException> iaeEx = new AtomicReference<>();
 134                 AtomicReference<NoSuchMethodException> nmeEx = new AtomicReference<>();
 135                 AtomicReference<Boolean> stopped = new AtomicReference<>(false);
 136 
 137                 Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
 138                     if (e instanceof InvocationTargetException) {
 139                         if (e.getCause() instanceof ThreadDeath) {
 140                             stopped.set(true);
 141                         } else {
 142                             iteEx.set((InvocationTargetException)e);
 143                         }
 144                     } else if (e instanceof IllegalAccessException) {
 145                         iaeEx.set((IllegalAccessException)e);
 146                     } else if (e instanceof NoSuchMethodException) {
 147                         nmeEx.set((NoSuchMethodException)e);
 148                     } else if (e instanceof ThreadDeath) {
 149                         stopped.set(true);
 150                     }
 151                 });
 152 
 153                 final Object[] res = new Object[1];
 154                 Thread snippetThread = new Thread(execThreadGroup, () -> {
 155                     try {
 156                         res[0] = doitMethod.invoke(null, new Object[0]);
 157                     } catch (InvocationTargetException e) {
 158                         if (e.getCause() instanceof ThreadDeath) {
 159                             stopped.set(true);
 160                         } else {
 161                             iteEx.set(e);
 162                         }
 163                     } catch (IllegalAccessException e) {
 164                         iaeEx.set(e);
 165                     } catch (ThreadDeath e) {
 166                         stopped.set(true);
 167                     }
 168                 });
 169 
 170                 snippetThread.start();
 171                 Thread[] threadList = new Thread[execThreadGroup.activeCount()];
 172                 execThreadGroup.enumerate(threadList);
 173                 for (Thread thread : threadList) {
 174                     if (thread != null)
 175                         thread.join();
 176                 }
 177 
 178                 if (stopped.get()) {
 179                     debug("Killed.");
 180 
 181                     return "";
 182                 }
 183 
 184                 if (iteEx.get() != null) {
 185                     throw iteEx.get();
 186                 } else if (nmeEx.get() != null) {
 187                     throw nmeEx.get();
 188                 } else if (iaeEx.get() != null) {
 189                     throw iaeEx.get();
 190                 }
 191 
 192                 return valueString(res[0]);
 193             } catch (InvocationTargetException ex) {
 194                 Throwable cause = ex.getCause();
 195                 StackTraceElement[] elems = cause.getStackTrace();
 196                 if (cause instanceof SPIResolutionException) {
 197                     int id = ((SPIResolutionException)cause).id();
 198 
 199                     throw execEnv.createUnresolvedReferenceException(id, elems);
 200                 } else {
 201                     throw execEnv.createEvalException(cause.getMessage() == null ?
 202                             "<none>" : cause.getMessage(), cause.getClass().getName(), elems);
 203                 }
 204             } catch (NoSuchMethodException | IllegalAccessException | InterruptedException ex) {
 205                 debug(ex, "Invoke failure");
 206             }
 207         } finally {
 208             synchronized (STOP_LOCK) {
 209                 userCodeRunning = false;
 210             }
 211         }
 212 
 213         return "";
 214     }
 215 
 216     @Override
 217     @SuppressWarnings("deprecation")
 218     public void stop() {
 219         synchronized (STOP_LOCK) {
 220             if (!userCodeRunning)
 221                 return;
 222 
 223             if (execThreadGroup == null) {
 224                 debug("Process-local code snippets thread group is null. Aborting stop.");
 225 
 226                 return;
 227             }
 228 
 229             execThreadGroup.stop();
 230         }
 231     }
 232 
 233     @Override
 234     public String varValue(String classname, String varname) {
 235         Class<?> klass = klasses.get(classname);
 236         if (klass == null) {
 237             debug("Var value failure: no such class loaded %s\n", classname);
 238 
 239             return "";
 240         }
 241         try {
 242             this.getClass().getModule().addReads(klass.getModule());
 243             Field var = klass.getDeclaredField(varname);
 244             var.setAccessible(true);
 245             Object res = var.get(null);
 246 
 247             return valueString(res);
 248         } catch (Exception ex) {
 249             debug("Var value failure: no such field %s.%s\n", classname, varname);
 250         }
 251 
 252         return "";
 253     }
 254 
 255     @Override
 256     public boolean addToClasspath(String cp) {
 257         // Append to the claspath
 258         for (String path : cp.split(File.pathSeparator)) {
 259             try {
 260                 loader.addURL(new File(path).toURI().toURL());
 261             } catch (MalformedURLException e) {
 262                 throw new InternalError("Classpath addition failed: " + cp, e);
 263             }
 264         }
 265 
 266         return true;
 267     }
 268 
 269     @Override
 270     public boolean redefine(Collection<String> classes) {
 271         return false;
 272     }
 273 
 274     @Override
 275     public ClassStatus getClassStatus(String classname) {
 276         if (!classBytes.containsKey(classname)) {
 277             return ClassStatus.UNKNOWN;
 278         } else if (!Arrays.equals(classBytes.get(classname), execEnv.getClassBytes(classname))) {
 279             return ClassStatus.NOT_CURRENT;
 280         } else {
 281             return ClassStatus.CURRENT;
 282         }
 283     }
 284 
 285     private void loadLocal(Collection<String> classes) throws ClassNotFoundException {
 286         for (String className : classes) {
 287             Class<?> klass = loader.loadClass(className);
 288             klasses.put(className, klass);
 289             classBytes.put(className, execEnv.getClassBytes(className));
 290             klass.getDeclaredMethods();
 291         }
 292     }
 293 
 294     private void debug(String format, Object... args) {
 295         //debug(execEnv.state(), execEnv.userErr(), flags, format, args);
 296     }
 297 
 298     private void debug(Exception ex, String where) {
 299         //debug(execEnv.state(), execEnv.userErr(), ex, where);
 300     }
 301 
 302     private static String valueString(Object value) {
 303         if (value == null) {
 304             return "null";
 305         } else if (value instanceof String) {
 306             return "\"" + (String)value + "\"";
 307         } else if (value instanceof Character) {
 308             return "'" + value + "'";
 309         } else {
 310             return value.toString();
 311         }
 312     }
 313 }