/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.idea.tests.gui.framework; import com.intellij.ide.BootstrapClassLoaderUtil; import com.intellij.ide.WindowsCommandLineProcessor; import com.intellij.idea.Main; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.application.ex.ApplicationEx; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.io.FileUtil; import com.intellij.util.PlatformUtils; import com.intellij.util.lang.ClassPath; import com.intellij.util.lang.ClasspathCache; import com.intellij.util.lang.UrlClassLoader; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; import java.util.List; import static com.android.tools.idea.tests.gui.framework.GuiTests.getProjectCreationDirPath; import static com.intellij.openapi.util.io.FileUtil.*; import static org.fest.reflect.core.Reflection.staticMethod; public class IdeTestApplication implements Disposable { private static final Logger LOG = Logger.getInstance(IdeTestApplication.class); private static IdeTestApplication ourInstance; @NotNull private final UrlClassLoader myIdeClassLoader; @NotNull public static synchronized IdeTestApplication getInstance() throws Exception { System.setProperty(PlatformUtils.PLATFORM_PREFIX_KEY, "AndroidStudio"); File configDirPath = getConfigDirPath(); System.setProperty(PathManager.PROPERTY_CONFIG_PATH, configDirPath.getPath()); // Force Swing FileChooser on Mac (instead of native one) to be able to use FEST to drive it. System.setProperty("native.mac.file.chooser.enabled", "false"); if (!isLoaded()) { ourInstance = new IdeTestApplication(); recreateDirectory(configDirPath); File newProjectsRootDirPath = getProjectCreationDirPath(); recreateDirectory(newProjectsRootDirPath); UrlClassLoader ideClassLoader = ourInstance.getIdeClassLoader(); Class<?> clazz = ideClassLoader.loadClass(GuiTests.class.getCanonicalName()); staticMethod("waitForIdeToStart").in(clazz).invoke(); staticMethod("setUpDefaultGeneralSettings").in(clazz).invoke(); } return ourInstance; } private static File getConfigDirPath() throws IOException { String homeDirPath = toSystemDependentName(PathManager.getHomePath()); assert !homeDirPath.isEmpty(); File configDirPath = new File(homeDirPath, FileUtil.join("androidStudio", "gui-tests", "config")); ensureExists(configDirPath); return configDirPath; } private static void recreateDirectory(@NotNull File path) throws IOException { delete(path); ensureExists(path); } private IdeTestApplication() throws Exception { String[] args = new String[0]; LOG.assertTrue(ourInstance == null, "Only one instance allowed."); ourInstance = this; pluginManagerStart(args); mainMain(); myIdeClassLoader = BootstrapClassLoaderUtil.initClassLoader(true); forceEagerClassPathLoading(); WindowsCommandLineProcessor.ourMirrorClass = Class.forName(WindowsCommandLineProcessor.class.getName(), true, myIdeClassLoader); // We set "GUI Testing Mode" on right away, even before loading the IDE. Class<?> androidPluginClass = Class.forName("org.jetbrains.android.AndroidPlugin", true, myIdeClassLoader); staticMethod("setGuiTestingMode").withParameterTypes(boolean.class) .in(androidPluginClass) .invoke(true); Class<?> pluginManagerClass = Class.forName("com.intellij.ide.plugins.PluginManager", true, myIdeClassLoader); staticMethod("start").withParameterTypes(String.class, String.class, String[].class) .in(pluginManagerClass) .invoke("com.intellij.idea.MainImpl", "start", args); } /** * We encountered a problem with {@link UrlClassLoader default IJ class loader} - it uses {@link ClassPath} which, in turn, * uses {@link ClasspathCache caching} by default and there is a race condition. Here are some facts about class loading implementation * details used by it: * <ol> * <li> * {@link ClassPath} lazily evaluates {@link ClassPath#push(List) configured classpath roots} when there is * {@link ClassPath#getResource(String, boolean) a request} for a resource and target resource hasn't been cached yet; * </li * <li> * {@link ClassPath} {@link ClasspathCache#nameSymbolsLoaded() seals loaded data} (reorganize it in a way to consume less memory) * when all {@link ClassPath#push(List) configured classpath roots} are processed; * </li> * <li> * Here is the problem - {@link ClasspathCache} state update from {@link ClasspathCache#myTempMapMode 'use temp map'} mode to * <code>'not use temp map'</code> mode is performed in not thread-safe manner; * </li> * <li> * Class loading is performed in a thread-safe manner (guaranteed by {@link ClassLoader} from the standard library. However, * resource loading doesn't imply any locks. E.g. we encountered a situation below: * <table> * <thead> * <tr> * <th>Thread1</th> * <th>Thread2</th> * </tr> * </thead> * <tbody> * <tr> * <td>{@link ClassPath#getResource(String, boolean)} is called for a particular class</td> * <td></td> * </tr> * <tr> * <td>{@link ClasspathCache#iterateLoaders(String, ClasspathCache.LoaderIterator, Object, Object)} is called as a result</td> * <td></td> * </tr> * <tr> * <td></td> * <td> * {@link ClassPath#getResource(String, boolean)} is called for a particular resource (not synced with the active * <code>'load class'</code> request * </td> * </tr> * <tr> * <td></td> * <td> * This request is a general purpose one (e.g. * <a href="http://docs.oracle.com/javase/tutorial/sound/SPI-intro.html">a call to custom service implementation</a>) * and target resource is not found in any of the configured classpath roots, effectively forcing {@link ClassPath} * to iterate (load) all of them; * </td> * </tr> * <tr> * <td></td> * <td> * {@link ClasspathCache#nameSymbolsLoaded()} is called when all configured classpath roots are processed during * an attempt to find target resource; * </td> * </tr> * <tr> * <td></td> * <td> * {@link ClasspathCache#myTempMapMode} is set to <code>false</code> as the very first thing during * {@link ClasspathCache#nameSymbolsLoaded()} processing, {@link ClasspathCache#myNameFilter} object is created and * its state population begins; * </td> * </tr> * <tr> * <td> * {@link ClasspathCache#iterateLoaders(String, ClasspathCache.LoaderIterator, Object, Object)} continues the processing * and calls {@link ClassPath.ResourceStringLoaderIterator#process(Loader, Object, Object)} which, in turn, * calls {@link ClasspathCache#loaderHasName(String, Loader)}; * </td> * <td></td> * </tr> * <tr> * <td> * And here lays the problem: {@link ClasspathCache#loaderHasName(String, Loader)} sees that * {@link ClasspathCache#myTempMapMode} is set to <code>false</code> and forwards the processing to the * {@link ClasspathCache#myNameFilter}. But it's state is still being updated, so, there is a possible case that it * returns <code>null</code> for a resource {@link ClasspathCache#addNameEntry(String, Loader) added previously}; * </td> * <td></td> * </tr> * </tbody> * </table> * </li> * </ol> * So, proper way to fix the problem is to address that race condition at the {@link ClassPath}/{@link ClasspathCache} level. * However, it's rather dangerous to just explicitly add synchronization there because JetBrains worked a lot on class loading * performance optimization and any change there requires thorough testing. * <p/> * That's why we did the following: * <ul> * <li>told JetBrains about the problem (effectively putting the burden of a proper fix on them);</li * <li> * do a kind of a hack here as a temporary solution - force eager {@link ClasspathCache#nameSymbolsLoaded() classpath cache sealing} * by requesting to load un-existing resource; * </li> * </ul> */ private void forceEagerClassPathLoading() { myIdeClassLoader.findResource("Really hope there is no resource with such name"); } private static void pluginManagerStart(@NotNull String[] args) { // Duplicates what PluginManager#start does. Main.setFlags(args); UIUtil.initDefaultLAF(); } private static void mainMain() { // Duplicates what Main#main does. staticMethod("installPatch").in(Main.class).invoke(); } @NotNull public UrlClassLoader getIdeClassLoader() { return myIdeClassLoader; } @Override public void dispose() { disposeInstance(); } public static synchronized void disposeInstance() { if (!isLoaded()) { return; } Application application = ApplicationManager.getApplication(); if (application != null) { if (application instanceof ApplicationEx) { ((ApplicationEx)application).exit(true, true); } else { application.exit(); } } ourInstance = null; } public static synchronized boolean isLoaded() { return ourInstance != null; } }