8696fa19500c3ab12bf98f1483452c27ce3cd42d
[idea/community.git] / plugins / groovy / jps-plugin / src / org / jetbrains / jps / incremental / groovy / InProcessGroovyc.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.jps.incremental.groovy;
17
18 import com.intellij.execution.process.ProcessOutputTypes;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.util.Condition;
21 import com.intellij.openapi.util.Key;
22 import com.intellij.openapi.util.Pair;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.reference.SoftReference;
26 import com.intellij.util.ConcurrencyUtil;
27 import com.intellij.util.SystemProperties;
28 import com.intellij.util.containers.ContainerUtil;
29 import com.intellij.util.lang.UrlClassLoader;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32 import org.jetbrains.groovy.compiler.rt.GroovyRtConstants;
33 import org.jetbrains.groovy.compiler.rt.ClassDependencyLoader;
34
35 import java.io.*;
36 import java.lang.reflect.Method;
37 import java.net.MalformedURLException;
38 import java.net.URISyntaxException;
39 import java.net.URL;
40 import java.net.URLClassLoader;
41 import java.util.*;
42 import java.util.concurrent.*;
43 import java.util.regex.Pattern;
44
45 /**
46  * @author peter
47  */
48 class InProcessGroovyc implements GroovycFlavor {
49   private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.incremental.groovy.InProcessGroovyc");
50   private static final Pattern GROOVY_ALL_JAR_PATTERN = Pattern.compile("groovy-all(-(.*))?\\.jar");
51   private static final ThreadPoolExecutor ourExecutor = ConcurrencyUtil.newSingleThreadExecutor("Groovyc");
52   private static SoftReference<Pair<String, ClassLoader>> ourParentLoaderCache;
53   private static final UrlClassLoader.CachePool ourLoaderCachePool = UrlClassLoader.createCachePool();
54   private final Collection<String> myOutputs;
55   private final boolean myHasStubExcludes;
56
57   InProcessGroovyc(Collection<String> outputs, boolean hasStubExcludes) {
58     myOutputs = outputs;
59     myHasStubExcludes = hasStubExcludes;
60   }
61
62   @Override
63   public GroovycContinuation runGroovyc(final Collection<String> compilationClassPath,
64                                         final boolean forStubs,
65                                         final JpsGroovySettings settings,
66                                         final File tempFile,
67                                         final GroovycOutputParser parser) throws Exception {
68     boolean jointPossible = forStubs && !myHasStubExcludes;
69     final LinkedBlockingQueue<String> mailbox = jointPossible && SystemProperties.getBooleanProperty("groovyc.joint.compilation", true)
70                                                 ? new LinkedBlockingQueue<String>() : null;
71
72     final JointCompilationClassLoader loader = createCompilationClassLoader(compilationClassPath);
73
74     final Future<Void> future = ourExecutor.submit(new Callable<Void>() {
75       @Override
76       public Void call() throws Exception {
77         runGroovycInThisProcess(loader, forStubs, settings, tempFile, parser, mailbox);
78         return null;
79       }
80     });
81     if (mailbox == null) {
82       future.get();
83       return null;
84     }
85
86     return waitForStubGeneration(future, mailbox, parser, loader);
87   }
88
89   @Nullable
90   private static GroovycContinuation waitForStubGeneration(final Future<Void> future,
91                                                            final LinkedBlockingQueue<String> mailbox,
92                                                            final GroovycOutputParser parser,
93                                                            JointCompilationClassLoader loader) throws InterruptedException {
94     while (true) {
95       if (future.isDone()) {
96         return null;
97       }
98
99       Object msg = mailbox.poll(10, TimeUnit.MILLISECONDS);
100       if (GroovyRtConstants.STUBS_GENERATED.equals(msg)) {
101         loader.resetCache();
102         return createContinuation(future, mailbox, parser);
103       }
104       if (msg != null) {
105         throw new AssertionError("Unknown message: " + msg);
106       }
107     }
108   }
109
110   @NotNull
111   private static GroovycContinuation createContinuation(final Future<Void> future,
112                                                         final LinkedBlockingQueue<String> mailbox,
113                                                         final GroovycOutputParser parser) {
114     return new GroovycContinuation() {
115       @NotNull
116       @Override
117       public GroovycOutputParser continueCompilation() throws Exception {
118         parser.onContinuation();
119         mailbox.offer(GroovyRtConstants.JAVAC_COMPLETED);
120         future.get();
121         return parser;
122       }
123
124       @Override
125       public void buildAborted() {
126         mailbox.offer(GroovyRtConstants.BUILD_ABORTED);
127       }
128     };
129   }
130
131   @SuppressWarnings("UseOfSystemOutOrSystemErr")
132   private static void runGroovycInThisProcess(ClassLoader loader,
133                                               boolean forStubs,
134                                               JpsGroovySettings settings,
135                                               File tempFile,
136                                               final GroovycOutputParser parser,
137                                               @Nullable Queue mailbox) throws Exception {
138     PrintStream oldOut = System.out;
139     PrintStream oldErr = System.err;
140     ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
141
142     System.setOut(createStream(parser, ProcessOutputTypes.STDOUT, oldOut));
143     System.setErr(createStream(parser, ProcessOutputTypes.STDERR, oldErr));
144     Thread.currentThread().setContextClassLoader(loader);
145     try {
146       Class<?> runnerClass = loader.loadClass("org.jetbrains.groovy.compiler.rt.GroovycRunner");
147       Method intMain = runnerClass.getDeclaredMethod("intMain2", boolean.class, boolean.class, boolean.class, String.class, Queue.class);
148       Integer exitCode = (Integer)intMain.invoke(null, settings.invokeDynamic, false, forStubs, tempFile.getPath(), mailbox);
149       parser.notifyFinished(exitCode);
150     }
151     catch (Exception e) {
152       throw new RuntimeException(e);
153     }
154     finally {
155       System.out.flush();
156       System.err.flush();
157
158       System.setOut(oldOut);
159       System.setErr(oldErr);
160       Thread.currentThread().setContextClassLoader(oldLoader);
161     }
162   }
163
164   @NotNull
165   private JointCompilationClassLoader createCompilationClassLoader(Collection<String> compilationClassPath) throws MalformedURLException {
166     ClassLoader parent = obtainParentLoader(compilationClassPath);
167     return new JointCompilationClassLoader(UrlClassLoader.build().
168       urls(toUrls(compilationClassPath)).parent(parent).allowLock().
169       useCache(ourLoaderCachePool, new UrlClassLoader.CachingCondition() {
170         @Override
171         public boolean shouldCacheData(@NotNull URL url) {
172           try {
173             String file = FileUtil.toCanonicalPath(new File(url.toURI()).getPath());
174             for (String output : myOutputs) {
175               if (FileUtil.startsWith(output, file)) {
176                 return false;
177               }
178             }
179             return true;
180           }
181           catch (URISyntaxException e) {
182             LOG.info(e);
183             return false;
184           }
185         }
186       }));
187   }
188
189   @Nullable
190   private static ClassLoader obtainParentLoader(Collection<String> compilationClassPath) throws MalformedURLException {
191     if (!"true".equals(System.getProperty("groovyc.reuse.compiler.classes", "true"))) {
192       return null;
193     }
194
195     String groovyAll = ContainerUtil.find(compilationClassPath, new Condition<String>() {
196       @Override
197       public boolean value(String s) {
198         return GROOVY_ALL_JAR_PATTERN.matcher(StringUtil.getShortName(s, '/')).matches();
199       }
200     });
201     if (groovyAll == null) {
202       return null;
203     }
204
205     Pair<String, ClassLoader> pair = SoftReference.dereference(ourParentLoaderCache);
206     if (pair != null && pair.first.equals(groovyAll)) {
207       return pair.second;
208     }
209
210     final ClassDependencyLoader checkWellFormed = new ClassDependencyLoader() {
211       @Override
212       protected void loadClassDependencies(Class aClass) throws ClassNotFoundException {
213         if (!isCompilerCoreClass(aClass.getName()) || !(aClass.getClassLoader() instanceof UrlClassLoader)) {
214           super.loadClassDependencies(aClass);
215         }
216       }
217
218       private boolean isCompilerCoreClass(String name) {
219         if (name.startsWith("groovyjarjar")) {
220           return true;
221         }
222         if (name.startsWith("org.codehaus.groovy.")) {
223           String tail = name.substring("org.codehaus.groovy.".length());
224           if (tail.startsWith("ast") ||
225               tail.startsWith("classgen") ||
226               tail.startsWith("tools.javac") ||
227               tail.startsWith("antlr") ||
228               tail.startsWith("vmplugin") ||
229               tail.startsWith("reflection") ||
230               tail.startsWith("control")) {
231             return true;
232           }
233           if (tail.startsWith("runtime") && name.contains("GroovyMethods")) {
234             return true;
235           }
236         }
237         return false;
238       }
239     };
240     UrlClassLoader groovyAllLoader = UrlClassLoader.build().
241       urls(toUrls(ContainerUtil.concat(GroovyBuilder.getGroovyRtRoots(), Collections.singletonList(groovyAll)))).allowLock().
242         useCache(ourLoaderCachePool, new UrlClassLoader.CachingCondition() {
243           @Override
244           public boolean shouldCacheData(
245             @NotNull URL url) {
246             return true;
247           }
248         }).get();
249     ClassLoader wrapper = new URLClassLoader(new URL[0], groovyAllLoader) {
250       @Override
251       protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
252         if (name.startsWith("groovy.grape.")) {
253           // grape depends on Ivy which is not included in this class loader
254           throw new ClassNotFoundException(name);
255         }
256         try {
257           return checkWellFormed.loadDependencies(super.loadClass(name, resolve));
258         }
259         catch (NoClassDefFoundError e) {
260           // We might attempt to load some class in groovy-all.jar that depends on a class from another library
261           // (e.g. GroovyTestCase extends TestCase).
262           // We don't want groovyc's resolve to stop at this point.
263           // Let's try in the child class loader which contains full compilation class with all libraries, including groovy-all.
264           // For this to happen we should throw ClassNotFoundException
265           throw new ClassNotFoundException(name, e);
266         }
267       }
268     };
269
270     ourParentLoaderCache = new SoftReference<Pair<String, ClassLoader>>(Pair.create(groovyAll, wrapper));
271     return wrapper;
272   }
273
274
275
276
277   @NotNull
278   private static List<URL> toUrls(Collection<String> paths) throws MalformedURLException {
279     List<URL> urls = ContainerUtil.newArrayList();
280     for (String s : paths) {
281       urls.add(new File(s).toURI().toURL());
282     }
283     return urls;
284   }
285
286   @NotNull
287   private static PrintStream createStream(final GroovycOutputParser parser, final Key type, final PrintStream overridden) {
288     final Thread thread = Thread.currentThread();
289     return new PrintStream(new OutputStream() {
290       ByteArrayOutputStream line = new ByteArrayOutputStream();
291       boolean hasLineSeparator = false;
292
293       @Override
294       public void write(int b) throws IOException {
295         if (Thread.currentThread() != thread) {
296           overridden.write(b);
297           return;
298         }
299
300         if (hasLineSeparator && !isLineSeparator(b)) {
301           flush();
302         }
303         else {
304           hasLineSeparator |= isLineSeparator(b);
305         }
306         line.write(b);
307       }
308
309       private boolean isLineSeparator(int b) {
310         return b == '\n' || b == '\r';
311       }
312
313       @Override
314       public void flush() throws IOException {
315         if (Thread.currentThread() != thread) {
316           overridden.flush();
317           return;
318         }
319
320         if (line.size() > 0) {
321           parser.notifyTextAvailable(StringUtil.convertLineSeparators(line.toString()), type);
322           line = new ByteArrayOutputStream();
323           hasLineSeparator = false;
324         }
325       }
326
327     });
328   }
329 }