View Javadoc

1   // Copyright (C) 2001 - 2012 Philip Aston
2   // Copyright (C) 2005 Martin Wagner
3   // All rights reserved.
4   //
5   // This file is part of The Grinder software distribution. Refer to
6   // the file LICENSE which is part of The Grinder distribution for
7   // licensing details. The Grinder distribution is available on the
8   // Internet at http://grinder.sourceforge.net/
9   //
10  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
11  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
12  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
13  // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
14  // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
15  // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
16  // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
17  // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
18  // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
19  // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
20  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
21  // OF THE POSSIBILITY OF SUCH DAMAGE.
22  
23  package net.grinder.scriptengine.jython;
24  
25  import java.io.File;
26  
27  import net.grinder.engine.common.EngineException;
28  import net.grinder.engine.common.ScriptLocation;
29  import net.grinder.scriptengine.ScriptEngineService;
30  import net.grinder.scriptengine.ScriptEngineService.ScriptEngine;
31  import net.grinder.scriptengine.ScriptEngineService.WorkerRunnable;
32  import net.grinder.scriptengine.ScriptExecutionException;
33  
34  import org.python.core.PyClass;
35  import org.python.core.PyException;
36  import org.python.core.PyObject;
37  import org.python.core.PyString;
38  import org.python.core.PySystemState;
39  import org.python.util.PythonInterpreter;
40  
41  
42  /**
43   * Wrap up the context information necessary to invoke a Jython script.
44   *
45   * Package scope.
46   *
47   * @author Philip Aston
48   */
49  final class JythonScriptEngine implements ScriptEngine {
50  
51    private static final String PYTHON_HOME = "python.home";
52    private static final String PYTHON_CACHEDIR = "python.cachedir";
53    private static final String CACHEDIR_DEFAULT_NAME = "cachedir";
54  
55    private static final String TEST_RUNNER_CALLABLE_NAME = "TestRunner";
56  
57    private final PySystemState m_systemState;
58    private final PythonInterpreter m_interpreter;
59    private final PyClass m_dieQuietly;  // The softly spoken Welshman.
60    private final String m_version;
61  
62    private final PyObject m_testRunnerFactory;
63  
64    /**
65     * Constructor for JythonScriptEngine.
66     * @param pySystemState Python system state.
67     *
68     * @throws EngineException If the script engine could not be created.
69     */
70    public JythonScriptEngine(final ScriptLocation script)
71        throws EngineException {
72  
73      // Work around Jython issue 1894900.
74      // If the python.cachedir has not been specified, and Jython is loaded
75      // via the manifest classpath or the jar in the lib directory is
76      // explicitly mentioned in the CLASSPATH, then set the cache directory to
77      // be alongside jython.jar.
78      if (System.getProperty(PYTHON_HOME) == null &&
79          System.getProperty(PYTHON_CACHEDIR) == null) {
80        final String classpath = System.getProperty("java.class.path");
81  
82        final File grinderJar = findFileInPath(classpath, "grinder.jar");
83        final File grinderJarDirectory =
84          grinderJar != null ? grinderJar.getParentFile() : new File(".");
85  
86        final File jythonJar = findFileInPath(classpath, "jython.jar");
87        final File jythonHome =
88          jythonJar != null ? jythonJar.getParentFile() : grinderJarDirectory;
89  
90        if (grinderJarDirectory == null && jythonJar == null ||
91            grinderJarDirectory != null &&
92            grinderJarDirectory.equals(jythonHome)) {
93          final File cacheDir = new File(jythonHome, CACHEDIR_DEFAULT_NAME);
94          System.setProperty("python.cachedir", cacheDir.getAbsolutePath());
95        }
96      }
97  
98      m_systemState = new PySystemState();
99      m_interpreter = new PythonInterpreter(null, m_systemState);
100 
101     m_interpreter.exec("class ___DieQuietly___: pass");
102     m_dieQuietly = (PyClass) m_interpreter.get("___DieQuietly___");
103 
104     String version;
105 
106     try {
107       version = PySystemState.class.getField("version").get(null).toString();
108     }
109     catch (final Exception e) {
110       version = "Unknown";
111     }
112 
113     m_version = version;
114 
115     // Prepend the script directory to the Python path. This matches the
116     // behaviour of the Jython interpreter.
117     m_systemState.path.insert(0,
118       new PyString(script.getFile().getParent()));
119 
120     // Additionally, add the working directory to the Python path. I think
121     // this will always be the same as the worker's CWD. Users expect to be
122     // able to import from the directory the agent is running in or (when the
123     // script has been distributed), the distribution directory.
124     m_systemState.path.insert(1,
125       new PyString(script.getDirectory().getFile().getPath()));
126 
127     try {
128       // Run the test script, script does global set up here.
129       m_interpreter.execfile(script.getFile().getPath());
130     }
131     catch (final PyException e) {
132       throw new JythonScriptExecutionException("initialising test script", e);
133     }
134 
135     // Find the callable that acts as a factory for test runner instances.
136     m_testRunnerFactory = m_interpreter.get(TEST_RUNNER_CALLABLE_NAME);
137 
138     if (m_testRunnerFactory == null || !m_testRunnerFactory.isCallable()) {
139       throw new JythonScriptExecutionException(
140         "There is no callable (class or function) named '" +
141         TEST_RUNNER_CALLABLE_NAME + "' in " + script);
142     }
143   }
144 
145   /**
146    * Find a file, given a search path.
147    *
148    * @param path The path to search.
149    * @param fileName Name of the jar file to find.
150    */
151   private static File findFileInPath(final String path, final String fileName) {
152 
153     for (final String pathEntry : path.split(File.pathSeparator)) {
154       final File file = new File(pathEntry);
155 
156      if (file.exists() && file.getName().equals(fileName)) {
157         return file;
158       }
159     }
160 
161     return null;
162   }
163 
164   /**
165    * {@inheritDoc}
166    */
167   @Override public WorkerRunnable createWorkerRunnable()
168     throws EngineException {
169 
170     final PyObject pyTestRunner;
171 
172     try {
173       // Script does per-thread initialisation here and
174       // returns a callable object.
175       pyTestRunner = m_testRunnerFactory.__call__();
176     }
177     catch (final PyException e) {
178       throw new JythonScriptExecutionException(
179         "creating per-thread TestRunner object", e);
180     }
181 
182     if (!pyTestRunner.isCallable()) {
183       throw new JythonScriptExecutionException(
184         "The result of '" + TEST_RUNNER_CALLABLE_NAME +
185         "()' is not callable");
186     }
187 
188     return new JythonWorkerRunnable(pyTestRunner);
189   }
190 
191   /**
192    * {@inheritDoc}
193    */
194   @Override public WorkerRunnable createWorkerRunnable(final Object testRunner)
195     throws EngineException {
196 
197     if (testRunner instanceof PyObject) {
198       final PyObject pyTestRunner = (PyObject) testRunner;
199 
200       if (pyTestRunner.isCallable()) {
201         return new JythonWorkerRunnable(pyTestRunner);
202       }
203     }
204 
205     throw new JythonScriptExecutionException(
206       "testRunner object is not callable");
207   }
208 
209   /**
210    * Shut down the engine.
211    *
212    * <p>
213    * We don't use m_interpreter.cleanup(), which delegates to
214    * PySystemState.callExitFunc, as callExitFunc logs problems to stderr.
215    * Instead we duplicate the callExitFunc behaviour raise our own exceptions.
216    * </p>
217    *
218    * @throws EngineException
219    *           If the engine could not be shut down.
220    */
221   @Override
222   public void shutdown() throws EngineException {
223 
224     final PyObject exitfunc = m_systemState.__findattr__("exitfunc");
225 
226     if (exitfunc != null) {
227       try {
228         exitfunc.__call__();
229       }
230       catch (final PyException e) {
231         throw new JythonScriptExecutionException(
232           "calling script exit function", e);
233       }
234     }
235   }
236 
237   /**
238    * Returns a description of the script engine for the log.
239    *
240    * @return The description.
241    */
242   @Override
243   public String getDescription() {
244     return "Jython " + m_version;
245   }
246 
247   /**
248    * Wrapper for script's TestRunner.
249    */
250   private final class JythonWorkerRunnable
251     implements ScriptEngineService.WorkerRunnable {
252 
253     private final PyObject m_testRunner;
254 
255     public JythonWorkerRunnable(final PyObject testRunner) {
256       m_testRunner = testRunner;
257     }
258 
259     @Override
260     public void run() throws ScriptExecutionException {
261 
262       try {
263         m_testRunner.__call__();
264       }
265       catch (final PyException e) {
266         throw new JythonScriptExecutionException("calling TestRunner", e);
267       }
268     }
269 
270     /**
271      * <p>
272      * Ensure that if the test runner has a {@code __del__} attribute, it is
273      * called when the thread is shutdown. Normally Jython defers this to the
274      * Java garbage collector, so we might have done something like
275      *
276      * <blockquote>
277      *
278      * <pre>
279      * m_testRunner = null;
280      * Runtime.getRuntime().gc();
281      *</pre>
282      *
283      * </blockquote>
284      *
285      * instead. However this would have a number of problems:
286      *
287      * <ol>
288      * <li>Some JVM's may chose not to finalise the test runner in response to
289      * {@code gc()}.</li>
290      * <li>{@code __del__} would be called by a GC thread.</li>
291      * <li>The standard Jython finalizer wrapping around {@code __del__} logs
292      * to {@code stderr}.</li>
293      * </ol>
294      * </p>
295      *
296      * <p>
297      * Instead, we call any {@code __del__} ourselves. After calling this
298      * method, the {@code PyObject} that underlies this class is made invalid.
299      * </p>
300      */
301     @Override
302     public void shutdown() throws ScriptExecutionException {
303 
304       final PyObject del = m_testRunner.__findattr__("__del__");
305 
306       if (del != null) {
307         try {
308           del.__call__();
309         }
310         catch (final PyException e) {
311           throw new JythonScriptExecutionException(
312             "deleting TestRunner instance", e);
313         }
314         finally {
315           // To avoid the (pretty small) chance of the test runner being
316           // finalised and __del__ being run twice, we disable it.
317 
318           // Unfortunately, Jython caches the __del__ attribute and makes
319           // it impossible to turn it off at a class level. Instead we do
320           // this:
321           m_testRunner.__setattr__("__class__", m_dieQuietly);
322         }
323       }
324     }
325   }
326 }