/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* 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.intellij.ide.startup.impl;
import com.intellij.ide.startup.StartupManagerEx;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.application.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.*;
import com.intellij.openapi.project.impl.ProjectLifecycleListener;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.roots.impl.ProjectRootManagerImpl;
import com.intellij.openapi.startup.StartupActivity;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.impl.local.FileWatcher;
import com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl;
import com.intellij.openapi.vfs.newvfs.RefreshQueue;
import com.intellij.ui.GuiUtils;
import com.intellij.util.SmartList;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.io.storage.HeavyProcessLatch;
import com.intellij.util.messages.MessageBusConnection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import org.jetbrains.ide.PooledThreadExecutor;
import java.io.FileNotFoundException;
import java.util.*;
public class StartupManagerImpl extends StartupManagerEx {
private static final Logger LOG = Logger.getInstance("#com.intellij.ide.startup.impl.StartupManagerImpl");
private final List<Runnable> myPreStartupActivities = Collections.synchronizedList(new LinkedList<Runnable>());
private final List<Runnable> myStartupActivities = Collections.synchronizedList(new LinkedList<Runnable>());
private final List<Runnable> myDumbAwarePostStartupActivities = Collections.synchronizedList(new LinkedList<Runnable>());
private final List<Runnable> myNotDumbAwarePostStartupActivities = Collections.synchronizedList(new LinkedList<Runnable>());
private boolean myPostStartupActivitiesPassed; // guarded by this
private volatile boolean myPreStartupActivitiesPassed;
private volatile boolean myStartupActivitiesRunning;
private volatile boolean myStartupActivitiesPassed;
private final Project myProject;
private boolean myInitialRefreshScheduled;
public StartupManagerImpl(Project project) {
myProject = project;
}
private void checkNonDefaultProject() {
LOG.assertTrue(!myProject.isDefault(), "Please don't register startup activities for the default project: they won't ever be run");
}
@Override
public void registerPreStartupActivity(@NotNull Runnable runnable) {
checkNonDefaultProject();
LOG.assertTrue(!myPreStartupActivitiesPassed, "Registering pre startup activity that will never be run");
myPreStartupActivities.add(runnable);
}
@Override
public void registerStartupActivity(@NotNull Runnable runnable) {
checkNonDefaultProject();
LOG.assertTrue(!myStartupActivitiesPassed, "Registering startup activity that will never be run");
myStartupActivities.add(runnable);
}
@Override
public synchronized void registerPostStartupActivity(@NotNull Runnable runnable) {
checkNonDefaultProject();
LOG.assertTrue(!myPostStartupActivitiesPassed, "Registering post-startup activity that will never be run:" +
" disposed=" + myProject.isDisposed() + "; open=" + myProject.isOpen() + "; passed=" + myStartupActivitiesPassed);
(DumbService.isDumbAware(runnable) ? myDumbAwarePostStartupActivities : myNotDumbAwarePostStartupActivities).add(runnable);
}
@Override
public boolean startupActivityRunning() {
return myStartupActivitiesRunning;
}
@Override
public boolean startupActivityPassed() {
return myStartupActivitiesPassed;
}
@Override
public synchronized boolean postStartupActivityPassed() {
return myPostStartupActivitiesPassed;
}
@SuppressWarnings("SynchronizeOnThis")
public void runStartupActivities() {
ApplicationManager.getApplication().runReadAction(() -> {
AccessToken token = HeavyProcessLatch.INSTANCE.processStarted("Running Startup Activities");
try {
runActivities(myPreStartupActivities);
// to avoid atomicity issues if runWhenProjectIsInitialized() is run at the same time
synchronized (this) {
myPreStartupActivitiesPassed = true;
myStartupActivitiesRunning = true;
}
runActivities(myStartupActivities);
synchronized (this) {
myStartupActivitiesRunning = false;
myStartupActivitiesPassed = true;
}
}
finally {
token.finish();
}
});
}
public void runPostStartupActivitiesFromExtensions() {
StartupActivity[] extensions = Extensions.getExtensions(StartupActivity.POST_STARTUP_ACTIVITY);
for (final StartupActivity extension : extensions) {
final Runnable runnable = () -> {
if (!myProject.isDisposed()) {
long start = System.currentTimeMillis();
extension.runActivity(myProject);
long duration = System.currentTimeMillis() - start;
if (duration > 200) {
LOG.info(extension.getClass().getSimpleName() + " run in " + duration);
}
}
};
if (extension instanceof DumbAware) {
runActivity(runnable);
}
else {
queueSmartModeActivity(runnable);
}
}
}
// queue each activity in smart mode separately so that if one of them starts dumb mode, the next ones just wait for it to finish
private void queueSmartModeActivity(final Runnable activity) {
DumbService.getInstance(myProject).runWhenSmart(() -> runActivity(activity));
}
public void runPostStartupActivities() {
if (postStartupActivityPassed()) {
return;
}
final Application app = ApplicationManager.getApplication();
if (!app.isHeadlessEnvironment()) {
checkFsSanity();
checkProjectRoots();
}
runActivities(myDumbAwarePostStartupActivities);
DumbService dumbService = DumbService.getInstance(myProject);
dumbService.runWhenSmart(new Runnable() {
@Override
public void run() {
app.assertIsDispatchThread();
// myDumbAwarePostStartupActivities might be non-empty if new activities were registered during dumb mode
runActivities(myDumbAwarePostStartupActivities);
while (true) {
List<Runnable> dumbUnaware = takeDumbUnawareStartupActivities();
if (dumbUnaware.isEmpty()) break;
dumbUnaware.forEach(StartupManagerImpl.this::queueSmartModeActivity);
}
if (dumbService.isDumb()) {
// return here later to process newly submitted activities (if any) and set myPostStartupActivitiesPassed
dumbService.runWhenSmart(this);
}
else {
//noinspection SynchronizeOnThis
synchronized (this) {
myPostStartupActivitiesPassed = true;
}
}
}
});
}
private synchronized List<Runnable> takeDumbUnawareStartupActivities() {
List<Runnable> result = new ArrayList<>(myNotDumbAwarePostStartupActivities);
myNotDumbAwarePostStartupActivities.clear();
return result;
}
public void scheduleInitialVfsRefresh() {
GuiUtils.invokeLaterIfNeeded(() -> {
if (myProject.isDisposed() || myInitialRefreshScheduled) return;
myInitialRefreshScheduled = true;
((ProjectRootManagerImpl)ProjectRootManager.getInstance(myProject)).markRootsForRefresh();
Application app = ApplicationManager.getApplication();
if (!app.isCommandLine()) {
final long sessionId = VirtualFileManager.getInstance().asyncRefresh(null);
final MessageBusConnection connection = app.getMessageBus().connect();
connection.subscribe(ProjectLifecycleListener.TOPIC, new ProjectLifecycleListener() {
@Override
public void afterProjectClosed(@NotNull Project project) {
if (project != myProject) return;
RefreshQueue.getInstance().cancelSession(sessionId);
connection.disconnect();
}
});
}
else {
VirtualFileManager.getInstance().syncRefresh();
}
}, ModalityState.defaultModalityState());
}
private void checkFsSanity() {
try {
String path = myProject.getBasePath();
if (path == null || FileUtil.isAncestor(PathManager.getConfigPath(), path, true)) {
return;
}
boolean expected = SystemInfo.isFileSystemCaseSensitive, actual = FileUtil.isFileSystemCaseSensitive(path);
LOG.info(path + " case-sensitivity: expected=" + expected + " actual=" + actual);
if (actual != expected) {
int prefix = expected ? 1 : 0; // IDE=true -> FS=false -> prefix='in'
String title = ApplicationBundle.message("fs.case.sensitivity.mismatch.title");
String text = ApplicationBundle.message("fs.case.sensitivity.mismatch.message", prefix);
Notifications.Bus.notify(
new Notification(Notifications.SYSTEM_MESSAGES_GROUP_ID, title, text, NotificationType.WARNING, NotificationListener.URL_OPENING_LISTENER),
myProject);
}
}
catch (FileNotFoundException e) {
LOG.warn(e);
}
}
private void checkProjectRoots() {
VirtualFile[] roots = ProjectRootManager.getInstance(myProject).getContentRoots();
if (roots.length == 0) return;
LocalFileSystem fs = LocalFileSystem.getInstance();
if (!(fs instanceof LocalFileSystemImpl)) return;
FileWatcher watcher = ((LocalFileSystemImpl)fs).getFileWatcher();
if (!watcher.isOperational()) return;
PooledThreadExecutor.INSTANCE.submit(() -> {
LOG.debug("FW/roots waiting started");
while (true) {
if (myProject.isDisposed()) return;
if (!watcher.isSettingRoots()) break;
TimeoutUtil.sleep(10);
}
LOG.debug("FW/roots waiting finished");
Collection<String> manualWatchRoots = watcher.getManualWatchRoots();
if (!manualWatchRoots.isEmpty()) {
List<String> nonWatched = new SmartList<>();
for (VirtualFile root : roots) {
if (!(root.getFileSystem() instanceof LocalFileSystem)) continue;
String rootPath = root.getPath();
for (String manualWatchRoot : manualWatchRoots) {
if (FileUtil.isAncestor(manualWatchRoot, rootPath, false)) {
nonWatched.add(rootPath);
}
}
}
if (!nonWatched.isEmpty()) {
String message = ApplicationBundle.message("watcher.non.watchable.project");
watcher.notifyOnFailure(message, null);
LOG.info("unwatched roots: " + nonWatched);
LOG.info("manual watches: " + manualWatchRoots);
}
}
});
}
public void startCacheUpdate() {
if (myProject.isDisposed()) return;
try {
DumbServiceImpl dumbService = DumbServiceImpl.getInstance(myProject);
if (!ApplicationManager.getApplication().isUnitTestMode()) {
// pre-startup activities have registered dumb tasks that load VFS (scanning files to index)
// only after these tasks pass does VFS refresh make sense
dumbService.queueTask(new DumbModeTask() {
@Override
public void performInDumbMode(@NotNull ProgressIndicator indicator) {
scheduleInitialVfsRefresh();
}
@Override
public String toString() {
return "initial refresh";
}
});
}
}
catch (ProcessCanceledException e) {
throw e;
}
catch (Throwable e) {
LOG.error(e);
}
}
private static void runActivities(@NotNull List<Runnable> activities) {
while (!activities.isEmpty()) {
runActivity(activities.remove(0));
}
}
private static void runActivity(Runnable runnable) {
ProgressManager.checkCanceled();
try {
runnable.run();
}
catch (ProcessCanceledException e) {
throw e;
}
catch (Throwable ex) {
LOG.error(ex);
}
}
@Override
public void runWhenProjectIsInitialized(@NotNull final Runnable action) {
final Application application = ApplicationManager.getApplication();
if (application == null) return;
Runnable runnable = () -> {
if (myProject.isDisposed()) return;
//noinspection SynchronizeOnThis
synchronized (this) {
// in tests which simulate project opening, post-startup activities could have been run already.
// Then we should act as if the project was initialized
boolean initialized = myProject.isInitialized() || myProject.isDefault() || application.isUnitTestMode() && myPostStartupActivitiesPassed;
if (!initialized) {
registerPostStartupActivity(action);
return;
}
}
action.run();
};
GuiUtils.invokeLaterIfNeeded(runnable, ModalityState.NON_MODAL);
}
@TestOnly
public synchronized void prepareForNextTest() {
myPreStartupActivities.clear();
myStartupActivities.clear();
myDumbAwarePostStartupActivities.clear();
myNotDumbAwarePostStartupActivities.clear();
}
@TestOnly
public synchronized void checkCleared() {
try {
assert myStartupActivities.isEmpty() : "Activities: " + myStartupActivities;
assert myDumbAwarePostStartupActivities.isEmpty() : "DumbAware Post Activities: " + myDumbAwarePostStartupActivities;
assert myNotDumbAwarePostStartupActivities.isEmpty() : "Post Activities: " + myNotDumbAwarePostStartupActivities;
assert myPreStartupActivities.isEmpty() : "Pre Activities: " + myPreStartupActivities;
}
finally {
prepareForNextTest();
}
}
}