fix "IDEA-221944 Deadlock on opening second project" and support preloading for proje...
[idea/community.git] / platform / platform-impl / src / com / intellij / idea / ApplicationLoader.kt
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 @file:JvmName("ApplicationLoader")
3 package com.intellij.idea
4
5 import com.intellij.diagnostic.*
6 import com.intellij.diagnostic.StartUpMeasurer.Phases
7 import com.intellij.featureStatistics.fusCollectors.LifecycleUsageTriggerCollector
8 import com.intellij.icons.AllIcons
9 import com.intellij.ide.*
10 import com.intellij.ide.customize.CustomizeIDEWizardDialog
11 import com.intellij.ide.customize.CustomizeIDEWizardStepsProvider
12 import com.intellij.ide.impl.OpenProjectTask
13 import com.intellij.ide.impl.ProjectUtil
14 import com.intellij.ide.plugins.*
15 import com.intellij.ide.ui.customization.CustomActionsSchema
16 import com.intellij.notification.Notification
17 import com.intellij.notification.NotificationType
18 import com.intellij.notification.Notifications
19 import com.intellij.openapi.actionSystem.IdeActions
20 import com.intellij.openapi.application.*
21 import com.intellij.openapi.application.impl.ApplicationImpl
22 import com.intellij.openapi.components.stateStore
23 import com.intellij.openapi.diagnostic.Logger
24 import com.intellij.openapi.keymap.KeymapManager
25 import com.intellij.openapi.keymap.impl.SystemShortcuts
26 import com.intellij.openapi.progress.ProcessCanceledException
27 import com.intellij.openapi.project.Project
28 import com.intellij.openapi.ui.DialogEarthquakeShaker
29 import com.intellij.openapi.util.IconLoader
30 import com.intellij.openapi.util.SystemInfo
31 import com.intellij.openapi.util.SystemPropertyBean
32 import com.intellij.openapi.util.io.FileUtilRt
33 import com.intellij.openapi.util.registry.RegistryKeyBean
34 import com.intellij.openapi.wm.IdeFrame
35 import com.intellij.openapi.wm.WeakFocusStackManager
36 import com.intellij.openapi.wm.WindowManager
37 import com.intellij.openapi.wm.ex.WindowManagerEx
38 import com.intellij.openapi.wm.impl.SystemDock
39 import com.intellij.openapi.wm.impl.WindowManagerImpl
40 import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeFrame
41 import com.intellij.platform.PlatformProjectOpenProcessor
42 import com.intellij.ui.AnimatedIcon
43 import com.intellij.ui.AppIcon
44 import com.intellij.ui.AppUIUtil
45 import com.intellij.ui.CustomProtocolHandler
46 import com.intellij.ui.mac.MacOSApplicationProvider
47 import com.intellij.ui.mac.foundation.Foundation
48 import com.intellij.ui.mac.touchbar.TouchBarsManager
49 import com.intellij.util.ArrayUtilRt
50 import com.intellij.util.concurrency.AppExecutorUtil
51 import com.intellij.util.io.exists
52 import com.intellij.util.ui.AsyncProcessIcon
53 import com.intellij.util.ui.accessibility.ScreenReader
54 import net.miginfocom.layout.PlatformDefaults
55 import org.jetbrains.annotations.ApiStatus
56 import java.awt.EventQueue
57 import java.awt.GraphicsEnvironment
58 import java.beans.PropertyChangeListener
59 import java.io.File
60 import java.io.IOException
61 import java.nio.file.Paths
62 import java.util.*
63 import java.util.concurrent.CompletableFuture
64 import java.util.concurrent.Future
65 import java.util.concurrent.atomic.AtomicReference
66 import javax.swing.JFrame
67 import javax.swing.JOptionPane
68 import kotlin.system.exitProcess
69
70 private val SAFE_JAVA_ENV_PARAMETERS = arrayOf(JetBrainsProtocolHandler.REQUIRED_PLUGINS_KEY)
71 private val LOG = Logger.getInstance("#com.intellij.idea.IdeaApplication")
72
73 private var filesToLoad: List<File> = emptyList()
74 private var wizardStepProvider: CustomizeIDEWizardStepsProvider? = null
75
76 private fun executeInitAppInEdt(rawArgs: Array<String>,
77                                 initAppActivity: Activity,
78                                 pluginDescriptorsFuture: CompletableFuture<List<IdeaPluginDescriptor>>) {
79   val args = processProgramArguments(rawArgs)
80   StartupUtil.patchSystem(LOG)
81   val headless = Main.isHeadless()
82   val app = initAppActivity.runChild("create app") {
83     ApplicationImpl(java.lang.Boolean.getBoolean(PluginManagerCore.IDEA_IS_INTERNAL_PROPERTY), false, headless, Main.isCommandLine())
84   }
85
86   val registerFuture = pluginDescriptorsFuture.thenApply {
87     initAppActivity.runChild("app component registration") {
88       app.registerComponents(it)
89     }
90     it
91   }
92
93   if (args.isEmpty()) {
94     startApp(app, IdeStarter(), initAppActivity, registerFuture, args)
95     return
96   }
97
98   // ApplicationStarter it is extension, so, to find starter, extensions must be registered prior to that
99   registerFuture.thenRunOrHandleError {
100     val starter = findStarter(args.first()) ?: IdeStarter()
101     if (Main.isHeadless() && !starter.isHeadless) {
102       Main.showMessage("Startup Error", "Application cannot start in headless mode", true)
103       exitProcess(Main.NO_GRAPHICS)
104     }
105
106     starter.premain(args)
107     startApp(app, starter, initAppActivity, registerFuture, args)
108   }
109 }
110
111 private fun startApp(app: ApplicationImpl, starter: ApplicationStarter, initAppActivity: Activity, registerFuture: CompletableFuture<List<IdeaPluginDescriptor>>, args: List<String>) {
112   // this code is here for one simple reason - here we have application,
113   // and after plugin loading we don't have - ApplicationManager.getApplication() can be used, but it doesn't matter
114   // but it is very important to call registerRegistryAndMessageBusAndComponent immediately after application creation
115   // and do not place any time-consuming code in between (e.g. showLicenseeInfoOnSplash)
116   val registerRegistryAndInitStoreFuture = registerRegistryAndInitStore(registerFuture, app)
117
118   val headless = app.isHeadlessEnvironment
119   if (!headless) {
120     initAppActivity.runChild("icon loader activation") {
121       // todo investigate why in test mode dummy icon manager is not suitable
122       IconLoader.activate()
123       IconLoader.setStrictGlobally(app.isInternal)
124     }
125   }
126
127   // preload services only after icon activation
128   val future = registerRegistryAndInitStoreFuture
129     .thenCompose {
130       val preloadServiceActivity = StartUpMeasurer.start("preload services")
131       app.preloadServices(it)
132         .thenRun(Runnable {
133           preloadServiceActivity.end()
134         })
135     }
136
137   if (!headless) {
138     if (SystemInfo.isMac) {
139       initAppActivity.runChild("mac app init") {
140         MacOSApplicationProvider.initApplication()
141       }
142
143       // ensure that TouchBarsManager is loaded before WelcomeFrame/project
144       // do not wait completion - it is thread safe and not required for application start
145       AppExecutorUtil.getAppExecutorService().execute {
146         ParallelActivity.PREPARE_APP_INIT.run("mac touchbar") {
147           Foundation.init()
148           TouchBarsManager.isTouchBarAvailable()
149         }
150       }
151     }
152
153     // disabled due to https://youtrack.jetbrains.com/issue/JBR-1399
154     //initAppActivity.runChild("showLicenseeInfoOnSplash") {
155     //  SplashManager.showLicenseeInfoOnSplash(LOG)
156     //}
157
158     preloadIcons()
159
160     initAppActivity.runChild("migLayout") {
161       //IDEA-170295
162       PlatformDefaults.setLogicalPixelBase(PlatformDefaults.BASE_FONT_SIZE)
163     }
164     WeakFocusStackManager.getInstance()
165   }
166
167   future.thenRunOrHandleError {
168     // this invokeLater() call is needed not only because current thread maybe not EDT, but to place the app starting code on a freshly minted IdeEventQueue instance
169     val placeOnEventQueueActivity = initAppActivity.startChild(Phases.PLACE_ON_EVENT_QUEUE)
170     EventQueue.invokeLater {
171       placeOnEventQueueActivity.end()
172       StartupUtil.installExceptionHandler()
173       initAppActivity.end()
174
175       app.loadComponents(SplashManager.getProgressIndicator())
176       if (!headless) {
177         addActivateAndWindowsCliListeners(app)
178       }
179
180       (TransactionGuard.getInstance() as TransactionGuardImpl).performUserActivity {
181         starter.main(ArrayUtilRt.toStringArray(args))
182       }
183
184       if (PluginManagerCore.isRunningFromSources()) {
185         AppExecutorUtil.getAppExecutorService().execute {
186           AppUIUtil.updateWindowIcon(JOptionPane.getRootFrame())
187         }
188       }
189     }
190
191     // execute in parallel to loading components - this functionality should be used only by plugin functionality,
192     // that used after start-up
193     ParallelActivity.PREPARE_APP_INIT.run("init system properties") {
194       SystemPropertyBean.initSystemProperties()
195     }
196   }
197 }
198
199 private fun preloadIcons() {
200   AppExecutorUtil.getAppExecutorService().execute {
201     AsyncProcessIcon("")
202     AnimatedIcon.Blinking(AllIcons.Ide.FatalError)
203     AnimatedIcon.FS()
204   }
205 }
206
207 @ApiStatus.Internal
208 fun registerRegistryAndInitStore(registerFuture: CompletableFuture<List<IdeaPluginDescriptor>>, app: ApplicationImpl): CompletableFuture<List<IdeaPluginDescriptorImpl>> {
209   return registerFuture
210     .thenCompose { plugins ->
211       val future = CompletableFuture.runAsync(Runnable {
212         ParallelActivity.PREPARE_APP_INIT.run("add registry keys") {
213           RegistryKeyBean.addKeysFromPlugins()
214         }
215       }, AppExecutorUtil.getAppExecutorService())
216
217       // yes, at this moment initSystemProperties or RegistryKeyBean.addKeysFromPlugins maybe not yet performed, but it doesn't affect because not used.
218       initConfigurationStore(app, null)
219
220       future
221         .thenApply {
222           @Suppress("UNCHECKED_CAST")
223           plugins as List<IdeaPluginDescriptorImpl>
224         }
225     }
226 }
227
228 private fun addActivateAndWindowsCliListeners(app: ApplicationImpl) {
229   StartupUtil.addExternalInstanceListener { args ->
230     val ref = AtomicReference<Future<CliResult>>()
231     app.invokeAndWait {
232       LOG.info("ApplicationImpl.externalInstanceListener invocation")
233       val realArgs = if (args.isEmpty()) args else args.subList(1, args.size)
234       val projectAndFuture = CommandLineProcessor.processExternalCommandLine(realArgs, args.firstOrNull())
235       ref.set(projectAndFuture.getSecond())
236       val frame = when (val project = projectAndFuture.getFirst()) {
237         null -> WindowManager.getInstance().findVisibleFrame()
238         else -> WindowManager.getInstance().getIdeFrame(project) as JFrame
239       }
240       if (frame != null) {
241         if (frame is IdeFrame) {
242           AppIcon.getInstance().requestFocus(frame as IdeFrame)
243         }
244         else {
245           frame.toFront()
246           DialogEarthquakeShaker.shake(frame)
247         }
248       }
249     }
250
251     ref.get()
252   }
253
254   MainRunner.LISTENER = WindowsCommandLineListener { currentDirectory, args ->
255     val argsList = args.toList()
256     LOG.info("Received external Windows command line: current directory $currentDirectory, command line $argsList")
257     if (argsList.isEmpty()) return@WindowsCommandLineListener 0
258     var state = app.defaultModalityState
259     for (starter in ApplicationStarter.EP_NAME.iterable) {
260       if (starter.canProcessExternalCommandLine() && argsList[0] == starter.commandName && starter.allowAnyModalityState()) {
261         state = app.anyModalityState
262       }
263     }
264
265     val ref = AtomicReference<Future<CliResult>>()
266     app.invokeAndWait({ ref.set(CommandLineProcessor.processExternalCommandLine(argsList, currentDirectory).getSecond()) }, state)
267     CliResult.getOrWrapFailure(ref.get(), 1).returnCode
268   }
269 }
270
271 fun initApplication(rawArgs: Array<String>, initUiTask: Future<*>?) {
272   val initAppActivity = MainRunner.startupStart.endAndStart(Phases.INIT_APP)
273   val pluginDescriptorsFuture = CompletableFuture<List<IdeaPluginDescriptor>>()
274   AppExecutorUtil.getAppExecutorService().execute {
275     initAppActivity.runChild("wait UI init") {
276       initUiTask?.get()
277     }
278     EventQueue.invokeLater {
279       executeInitAppInEdt(rawArgs, initAppActivity, pluginDescriptorsFuture)
280     }
281     initAppActivity.runChild("load system fonts") {
282       // editor and other UI components need the list of system fonts to implement font fallback
283       // this list is pre-loaded here, in parallel to other activities, to speed up project opening
284       // ideally, it shouldn't overlap with other font-related activities to avoid contention on JDK-internal font manager locks
285       loadSystemFonts()
286     }
287   }
288
289   val plugins = try {
290     initAppActivity.runChild("plugin descriptors loading") {
291       PluginManagerCore.getLoadedPlugins(MainRunner::class.java.classLoader)
292     }
293   }
294   catch (e: Throwable) {
295     pluginDescriptorsFuture.completeExceptionally(e)
296     return
297   }
298
299   pluginDescriptorsFuture.complete(plugins)
300 }
301
302 private fun loadSystemFonts() = GraphicsEnvironment.getLocalGraphicsEnvironment().availableFontFamilyNames
303
304 fun findStarter(key: String): ApplicationStarter? {
305   for (starter in ApplicationStarter.EP_NAME.iterable) {
306     if (starter == null) {
307       break
308     }
309
310     if (starter.commandName == key) {
311       return starter
312     }
313   }
314   return null
315 }
316
317 fun openFilesOnLoading(files: List<File>) {
318   filesToLoad = files
319 }
320
321 fun setWizardStepsProvider(provider: CustomizeIDEWizardStepsProvider) {
322   wizardStepProvider = provider
323 }
324
325 fun initConfigurationStore(app: ApplicationImpl, configPath: String?) {
326   val beforeApplicationLoadedActivity = StartUpMeasurer.start("beforeApplicationLoaded")
327   val effectiveConfigPath = FileUtilRt.toSystemIndependentName(configPath ?: PathManager.getConfigPath())
328   for (listener in ApplicationLoadListener.EP_NAME.iterable) {
329     try {
330       (listener ?: break).beforeApplicationLoaded(app, effectiveConfigPath)
331     }
332     catch (e: ProcessCanceledException) {
333       throw e
334     }
335     catch (e: Throwable) {
336       LOG.error(e)
337     }
338   }
339
340   val initStoreActivity = beforeApplicationLoadedActivity.endAndStart("init app store")
341
342   // we set it after beforeApplicationLoaded call, because app store can depends on stream provider state
343   app.stateStore.setPath(effectiveConfigPath)
344   LoadingPhase.setCurrentPhase(LoadingPhase.CONFIGURATION_STORE_INITIALIZED)
345   initStoreActivity.end()
346 }
347
348 open class IdeStarter : ApplicationStarter {
349   override fun isHeadless() = false
350
351   override fun getCommandName(): String? = null
352
353   override fun canProcessExternalCommandLine() = true
354
355   override fun processExternalCommandLineAsync(args: List<String>, currentDirectory: String?): Future<CliResult> {
356     LOG.info("Request to open in $currentDirectory with parameters: ${args.joinToString(separator = ",")}")
357     if (args.isEmpty()) {
358       return CliResult.ok()
359     }
360
361     val filename = args[0]
362     val file = if (currentDirectory == null) Paths.get(filename) else Paths.get(currentDirectory, filename)
363     if (file.exists()) {
364       var line = -1
365       if (args.size > 2 && CustomProtocolHandler.LINE_NUMBER_ARG_NAME == args[1]) {
366         try {
367           line = args[2].toInt()
368         }
369         catch (ignore: NumberFormatException) {
370           LOG.error("Wrong line number: ${args[2]}")
371         }
372
373       }
374       PlatformProjectOpenProcessor.doOpenProject(file, OpenProjectTask(), line)
375     }
376     return CliResult.error(1, "Can't find file: $file")
377   }
378
379   private fun loadProjectFromExternalCommandLine(commandLineArgs: List<String>): Project? {
380     var project: Project? = null
381     if (commandLineArgs.firstOrNull() != null) {
382       LOG.info("ApplicationLoader.loadProject")
383       val currentDirectory: String? = System.getenv("IDEA_INITIAL_DIRECTORY")
384       LOG.info("IDEA_INITIAL_DIRECTORY: $currentDirectory")
385       project = CommandLineProcessor.processExternalCommandLine(commandLineArgs, currentDirectory).getFirst()
386     }
387     return project
388   }
389
390   override fun main(args: Array<String>) {
391     val frameInitActivity = StartUpMeasurer.start(Phases.FRAME_INITIALIZATION)
392
393     val app = ApplicationManager.getApplication()
394     // Event queue should not be changed during initialization of application components.
395     // It also cannot be changed before initialization of application components because IdeEventQueue uses other
396     // application components. So it is proper to perform replacement only here.
397     frameInitActivity.runChild("set window manager") {
398       IdeEventQueue.getInstance().setWindowManager(WindowManager.getInstance() as WindowManagerImpl)
399     }
400
401     val commandLineArgs = args.toList()
402
403     val appFrameCreatedActivity = frameInitActivity.startChild("call appFrameCreated")
404     val lifecyclePublisher = app.messageBus.syncPublisher(AppLifecycleListener.TOPIC)
405     lifecyclePublisher.appFrameCreated(commandLineArgs)
406     appFrameCreatedActivity.end()
407
408     // must be after appFrameCreated because some listeners can mutate state of RecentProjectsManager
409     val willOpenProject = commandLineArgs.isNotEmpty() || filesToLoad.isNotEmpty() || RecentProjectsManager.getInstance().willReopenProjectOnStart()
410
411     // temporary check until the JRE implementation has been checked and bundled
412     if (java.lang.Boolean.getBoolean("ide.popup.enablePopupType")) {
413       @Suppress("SpellCheckingInspection")
414       System.setProperty("jbre.popupwindow.settype", "true")
415     }
416
417     val shouldShowWelcomeFrame = !willOpenProject || JetBrainsProtocolHandler.getCommand() != null
418     val doShowWelcomeFrame = if (shouldShowWelcomeFrame) WelcomeFrame.prepareToShow() else null
419     showWizardAndWelcomeFrame(when (doShowWelcomeFrame) {
420       null -> null
421       else -> Runnable {
422         doShowWelcomeFrame.run()
423         lifecyclePublisher.welcomeScreenDisplayed()
424       }
425     })
426
427     frameInitActivity.end()
428
429     AppExecutorUtil.getAppExecutorService().run {
430       LifecycleUsageTriggerCollector.onIdeStart()
431     }
432
433     TransactionGuard.submitTransaction(app, Runnable {
434       val project = when {
435         filesToLoad.isNotEmpty() -> ProjectUtil.tryOpenFileList(null, filesToLoad, "MacMenu")
436         commandLineArgs.isNotEmpty() -> loadProjectFromExternalCommandLine(commandLineArgs)
437         else -> null
438       }
439
440       app.messageBus.syncPublisher(AppLifecycleListener.TOPIC).appStarting(project)
441
442       if (project == null && !JetBrainsProtocolHandler.appStartedWithCommand()) {
443         RecentProjectsManager.getInstance().reopenLastProjectsOnStart()
444       }
445
446       EventQueue.invokeLater {
447         reportPluginError()
448       }
449     })
450
451     if (!app.isHeadlessEnvironment) {
452       postOpenUiTasks(app)
453     }
454   }
455
456   private fun showWizardAndWelcomeFrame(showWelcomeFrame: Runnable?) {
457     wizardStepProvider?.let { wizardStepsProvider ->
458       val wizardDialog = object : CustomizeIDEWizardDialog(wizardStepsProvider, null, false, true) {
459         override fun doOKAction() {
460           super.doOKAction()
461           showWelcomeFrame?.run()
462         }
463       }
464       if (wizardDialog.showIfNeeded()) {
465         return
466       }
467     }
468     showWelcomeFrame?.run()
469   }
470
471   private fun postOpenUiTasks(app: Application) {
472     if (SystemInfo.isMac) {
473       ApplicationManager.getApplication().executeOnPooledThread {
474         TouchBarsManager.onApplicationInitialized()
475         if (TouchBarsManager.isTouchBarAvailable()) {
476           CustomActionsSchema.addSettingsGroup(IdeActions.GROUP_TOUCHBAR, "Touch Bar")
477         }
478
479         val keymap = KeymapManager.getInstance().activeKeymap
480         SystemShortcuts().checkConflictsAndNotify(keymap)
481       }
482     }
483
484     app.invokeLater {
485       val updateSystemDockActivity = StartUpMeasurer.start("system dock menu")
486       SystemDock.updateMenu()
487       updateSystemDockActivity.end()
488     }
489     app.invokeLater {
490       val generalSettings = GeneralSettings.getInstance()
491       generalSettings.addPropertyChangeListener(GeneralSettings.PROP_SUPPORT_SCREEN_READERS, app,
492                                                 PropertyChangeListener { e -> ScreenReader.setActive(e.newValue as Boolean) })
493       ScreenReader.setActive(generalSettings.isSupportScreenReaders)
494     }
495   }
496 }
497
498 /**
499  * Method looks for `-Dkey=value` program arguments and stores some of them in system properties.
500  * We should use it for a limited number of safe keys.
501  * One of them is a list of ids of required plugins
502  *
503  * @see SAFE_JAVA_ENV_PARAMETERS
504  */
505 @Suppress("SpellCheckingInspection")
506 private fun processProgramArguments(args: Array<String>): List<String> {
507   if (args.isEmpty()) {
508     return emptyList()
509   }
510
511   val arguments = mutableListOf<String>()
512   val safeKeys = SAFE_JAVA_ENV_PARAMETERS.toList()
513   for (arg in args) {
514     if (arg.startsWith("-D")) {
515       val keyValue = arg.substring(2).split('=')
516       if (keyValue.size == 2 && safeKeys.contains(keyValue[0])) {
517         System.setProperty(keyValue[0], keyValue[1])
518         continue
519       }
520     }
521     if (SplashManager.NO_SPLASH == arg) {
522       continue
523     }
524
525     arguments.add(arg)
526   }
527   return arguments
528 }
529
530 private fun CompletableFuture<*>.thenRunOrHandleError(handler: () -> Unit): CompletableFuture<Void>? {
531   return thenRun(handler)
532     .exceptionally {
533       MainRunner.processException(it)
534       null
535     }
536 }
537
538 private fun reportPluginError() {
539   if (PluginManagerCore.myPluginError == null) {
540     return
541   }
542
543   val title = IdeBundle.message("title.plugin.error")
544   Notifications.Bus.notify(Notification(title, title, PluginManagerCore.myPluginError, NotificationType.ERROR) { notification, event ->
545     notification.expire()
546
547     val description = event.description
548     if (PluginManagerCore.EDIT == description) {
549       val ideFrame = WindowManagerEx.getInstanceEx().findFrameFor(null)
550       PluginManagerConfigurableProxy.showPluginConfigurable(ideFrame?.component, null)
551       return@Notification
552     }
553
554     val disabledPlugins = LinkedHashSet(PluginManagerCore.disabledPlugins())
555     if (PluginManagerCore.myPlugins2Disable != null && PluginManagerCore.DISABLE == description) {
556       disabledPlugins.addAll(PluginManagerCore.myPlugins2Disable)
557     }
558     else if (PluginManagerCore.myPlugins2Enable != null && PluginManagerCore.ENABLE == description) {
559       disabledPlugins.removeAll(PluginManagerCore.myPlugins2Enable)
560       PluginManagerMain.notifyPluginsUpdated(null)
561     }
562
563     try {
564       PluginManagerCore.saveDisabledPlugins(disabledPlugins, false)
565     }
566     catch (ignore: IOException) {
567     }
568
569     PluginManagerCore.myPlugins2Enable = null
570     PluginManagerCore.myPlugins2Disable = null
571   })
572   PluginManagerCore.myPluginError = null
573 }