Skip to content

Commit 72e3de8

Browse files
committed
Support Kotlin plugin updater
1 parent 2bc2615 commit 72e3de8

File tree

5 files changed

+316
-3
lines changed

5 files changed

+316
-3
lines changed

kotlin-eclipse-ui/META-INF/MANIFEST.MF

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ Require-Bundle: org.eclipse.ui,
2020
org.eclipse.search,
2121
org.eclipse.jdt.core.manipulation,
2222
org.eclipse.ltk.core.refactoring,
23-
org.eclipse.ltk.ui.refactoring
23+
org.eclipse.ltk.ui.refactoring,
24+
org.eclipse.equinox.p2.operations,
25+
org.eclipse.equinox.p2.core,
26+
org.eclipse.equinox.p2.metadata,
27+
org.eclipse.equinox.p2.engine,
28+
org.eclipse.mylyn.commons.ui,
29+
org.eclipse.equinox.p2.director
2430
Bundle-ActivationPolicy: lazy
2531
Import-Package: com.intellij.navigation,
2632
org.eclipse.core.expressions,

kotlin-eclipse-ui/Run Kotlin Plugin with Equinox Weaving.launch

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<booleanAttribute key="clearwslog" value="false"/>
1212
<stringAttribute key="configLocation" value="${workspace_loc}/.metadata/.plugins/org.eclipse.pde.core/Run Kotlin Plugin with Equinox Weaving"/>
1313
<booleanAttribute key="default" value="true"/>
14+
<booleanAttribute key="generateProfile" value="true"/>
1415
<booleanAttribute key="includeOptional" value="true"/>
1516
<stringAttribute key="location" value="${workspace_loc}/../runtime-EclipseApplicationwithEquinoxWeaving"/>
1617
<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
@@ -27,4 +28,4 @@
2728
<booleanAttribute key="useDefaultConfigArea" value="true"/>
2829
<booleanAttribute key="useProduct" value="true"/>
2930
<booleanAttribute key="usefeatures" value="false"/>
30-
</launchConfiguration>
31+
</launchConfiguration>

kotlin-eclipse-ui/src/org/jetbrains/kotlin/preferences/KotlinPreferenceInitializer.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,16 @@
1717
package org.jetbrains.kotlin.preferences
1818

1919
import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer
20+
import org.jetbrains.kotlin.ui.Activator
21+
import org.jetbrains.kotlin.ui.KotlinPluginUpdater
22+
import java.util.Random
2023

21-
public class KotlinPreferenceInitializer : AbstractPreferenceInitializer() {
24+
class KotlinPreferenceInitializer : AbstractPreferenceInitializer() {
2225
override fun initializeDefaultPreferences() {
26+
val kotlinStore = Activator.getDefault().preferenceStore
27+
with(kotlinStore) {
28+
setDefault(KotlinPluginUpdater.LAST_UPDATE_CHECK, 0L)
29+
setDefault(KotlinPluginUpdater.USER_ID, 0L)
30+
}
2331
}
2432
}
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
/*******************************************************************************
2+
* Copyright 2000-2016 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+
*******************************************************************************/
17+
package org.jetbrains.kotlin.ui
18+
19+
import java.util.concurrent.TimeUnit
20+
import org.eclipse.core.runtime.IProgressMonitor
21+
import org.eclipse.equinox.p2.metadata.IInstallableUnit
22+
import org.eclipse.equinox.p2.operations.OperationFactory
23+
import org.eclipse.core.runtime.NullProgressMonitor
24+
import java.net.URI
25+
import org.eclipse.jface.dialogs.MessageDialog
26+
import org.eclipse.swt.widgets.Display
27+
import org.eclipse.equinox.p2.operations.ProfileModificationJob
28+
import org.eclipse.equinox.p2.operations.ProvisioningJob
29+
import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup
30+
import org.eclipse.swt.widgets.Composite
31+
import org.eclipse.swt.layout.GridLayout
32+
import org.eclipse.swt.widgets.Label
33+
import org.eclipse.swt.SWT
34+
import org.eclipse.swt.layout.GridData
35+
import org.eclipse.swt.widgets.Link
36+
import org.eclipse.swt.events.SelectionAdapter
37+
import org.eclipse.swt.events.SelectionEvent
38+
import org.eclipse.equinox.p2.operations.UpdateOperation
39+
import java.net.URL
40+
import java.net.URLConnection
41+
import java.net.HttpURLConnection
42+
import org.jetbrains.kotlin.core.log.KotlinLogger
43+
import com.intellij.openapi.util.SystemInfo
44+
import java.net.URLEncoder
45+
import org.eclipse.jface.preference.IPreferenceStore
46+
import org.eclipse.equinox.p2.metadata.Version
47+
import java.util.Random
48+
import org.jetbrains.kotlin.eclipse.ui.utils.runJob
49+
import org.eclipse.core.runtime.IStatus
50+
import org.eclipse.core.runtime.Status
51+
import java.io.IOException
52+
import org.eclipse.core.runtime.jobs.JobChangeAdapter
53+
import org.eclipse.core.runtime.jobs.IJobChangeEvent
54+
import org.eclipse.ui.PlatformUI
55+
56+
private sealed class PluginUpdateStatus() {
57+
object Update : PluginUpdateStatus()
58+
59+
object CheckUpdateSiteFailed : PluginUpdateStatus()
60+
61+
class UpdateFailed(val message: String) : PluginUpdateStatus()
62+
}
63+
64+
public object KotlinPluginUpdater {
65+
val LAST_UPDATE_CHECK = "kotlin.lastUpdateCheck"
66+
val USER_ID = "kotlin.userId"
67+
68+
private val KOTLIN_GROUP_ID = "org.jetbrains.kotlin.feature.feature.group"
69+
private val ECLIPSE_PLATFORM_ID = "org.eclipse.platform"
70+
71+
private val MAIN_DELAY: Long = TimeUnit.DAYS.toMillis(1)
72+
private val retryState = RetryState(TimeUnit.HOURS.toMillis(3))
73+
74+
fun kotlinFileEdited() {
75+
val kotlinStore = Activator.getDefault().preferenceStore
76+
77+
val updateStatus: PluginUpdateStatus
78+
if (retryState.retryToUpdate()) {
79+
updateStatus = tryToUpdate(kotlinStore)
80+
} else {
81+
val lastUpdateTime = kotlinStore.getLong(LAST_UPDATE_CHECK)
82+
if (lastUpdateTime == 0L || checkTimeIsUp(lastUpdateTime, MAIN_DELAY)) {
83+
updateStatus = tryToUpdate(kotlinStore)
84+
} else {
85+
return
86+
}
87+
}
88+
89+
processResults(updateStatus, kotlinStore)
90+
}
91+
92+
private fun getKotlinInstallationUnit(monitor: IProgressMonitor): IInstallableUnit? {
93+
return OperationFactory().listInstalledElements(true, monitor).find { it.id == KOTLIN_GROUP_ID }
94+
}
95+
96+
private fun checkTimeIsUp(lastTry: Long, delay: Long): Boolean = System.currentTimeMillis() - lastTry > delay
97+
98+
private fun processResults(updateStatus: PluginUpdateStatus, kotlinStore: IPreferenceStore) {
99+
when (updateStatus) {
100+
is PluginUpdateStatus.Update -> {
101+
kotlinStore.setValue(LAST_UPDATE_CHECK, System.currentTimeMillis())
102+
retryState.disableRetry()
103+
}
104+
105+
is PluginUpdateStatus.CheckUpdateSiteFailed -> retryState.setUpForRetry()
106+
107+
is PluginUpdateStatus.UpdateFailed -> {
108+
KotlinLogger.logWarning("Could not check for update (${updateStatus.message})")
109+
retryState.setUpForRetry()
110+
}
111+
}
112+
}
113+
114+
private fun tryToUpdate(store: IPreferenceStore): PluginUpdateStatus {
115+
val monitor = NullProgressMonitor()
116+
val kotlinUnit = getKotlinInstallationUnit(monitor)
117+
if (kotlinUnit == null) {
118+
return PluginUpdateStatus.UpdateFailed("There is no kotlin feature group with id: $KOTLIN_GROUP_ID")
119+
}
120+
121+
val os = URLEncoder.encode(SystemInfo.OS_NAME + " " + SystemInfo.OS_VERSION, "UTF-8")
122+
val pluginVersion = kotlinUnit.version
123+
val uid = getOrSetUID(store)
124+
val eclipseVersion = getEclipsePlatformVersion(monitor)
125+
if (eclipseVersion == null) {
126+
KotlinLogger.logWarning("There is no eclipse platform group with id: $ECLIPSE_PLATFORM_ID")
127+
}
128+
129+
val params = "build=$eclipseVersion&pluginVersion=$pluginVersion&os=$os&uuid=$uid"
130+
val updateSiteStatus = obtainUpdateSite("https://plugins.jetbrains.com/eclipse-plugins/kotlin/update?$params")
131+
132+
return when (updateSiteStatus) {
133+
is UpdateSiteStatus.UpdateSite -> {
134+
val resolvedUpdateSite = updateSiteStatus.updateSite.removeSuffix("compositeArtifacts.xml")
135+
proposeToUpdate(kotlinUnit, resolvedUpdateSite, monitor)
136+
}
137+
138+
is UpdateSiteStatus.UpdateSiteNotFound -> PluginUpdateStatus.CheckUpdateSiteFailed
139+
140+
is UpdateSiteStatus.Failed -> PluginUpdateStatus.UpdateFailed("Could not obtain update site")
141+
}
142+
}
143+
144+
private fun proposeToUpdate(kotlinUnit: IInstallableUnit, updateSite: String, monitor: IProgressMonitor): PluginUpdateStatus {
145+
val uri = URI(updateSite)
146+
val updateOperation = OperationFactory().createUpdateOperation(listOf(kotlinUnit), listOf(uri), monitor)
147+
val result = updateOperation.resolveModal(monitor)
148+
if (result.isOK) {
149+
Display.getDefault().asyncExec {
150+
val updateNotification = UpdatePluginNotification(updateOperation, monitor, Display.getDefault())
151+
updateNotification.open()
152+
}
153+
}
154+
155+
return PluginUpdateStatus.Update
156+
}
157+
158+
private fun getOrSetUID(store: IPreferenceStore): Long {
159+
val userId = store.getLong(USER_ID)
160+
if (userId == 0L) {
161+
val generatedId = Random().nextLong()
162+
store.setValue(USER_ID, generatedId)
163+
return generatedId
164+
}
165+
166+
return userId
167+
}
168+
169+
private fun getEclipsePlatformVersion(monitor: IProgressMonitor): Version? {
170+
return OperationFactory().listInstalledElements(false, monitor)
171+
.find { it.id == ECLIPSE_PLATFORM_ID }
172+
?.version
173+
}
174+
175+
private fun obtainUpdateSite(urlString: String): UpdateSiteStatus {
176+
val url = URL(urlString)
177+
val connection = url.openConnection()
178+
if (connection is HttpURLConnection) {
179+
try {
180+
connection.setInstanceFollowRedirects(false)
181+
connection.connect()
182+
183+
if (connection.getResponseCode() == 302) {
184+
val redirect = connection.getHeaderField("Location")
185+
return if (redirect != null) UpdateSiteStatus.UpdateSite(redirect) else UpdateSiteStatus.Failed
186+
}
187+
} catch (e: IOException) {
188+
return UpdateSiteStatus.UpdateSiteNotFound
189+
} finally {
190+
connection.disconnect()
191+
}
192+
}
193+
194+
return UpdateSiteStatus.Failed
195+
}
196+
197+
private sealed class UpdateSiteStatus {
198+
object UpdateSiteNotFound : UpdateSiteStatus()
199+
200+
object Failed : UpdateSiteStatus()
201+
202+
class UpdateSite(val updateSite: String) : UpdateSiteStatus()
203+
}
204+
205+
private class RetryState(private val retryDelay: Long) {
206+
@Volatile private var lastRetryTime: Long = 0
207+
@Volatile private var isActive: Boolean = false
208+
209+
@Synchronized fun setUpForRetry() {
210+
isActive = true
211+
lastRetryTime = System.currentTimeMillis()
212+
}
213+
214+
fun disableRetry() {
215+
isActive = false
216+
}
217+
218+
fun retryToUpdate(): Boolean {
219+
return isActive && checkTimeIsUp(lastRetryTime, retryDelay)
220+
}
221+
}
222+
}
223+
224+
private class UpdatePluginNotification(
225+
val updateOperation: UpdateOperation,
226+
val monitor: IProgressMonitor,
227+
val display: Display) : AbstractNotificationPopup(display) {
228+
init {
229+
setDelayClose(0) // Don't close popup automatically
230+
}
231+
232+
override fun createContentArea(parent: Composite) {
233+
parent.setLayout(GridLayout(1, true))
234+
235+
val textLabel = Label(parent, SWT.LEFT)
236+
val installbleUnit = getInstallableUnit()
237+
textLabel.setText("A new version ${installbleUnit.version} of the Kotlin plugin is available.")
238+
textLabel.setLayoutData(gridData())
239+
240+
val updateLink = Link(parent, SWT.RIGHT)
241+
updateLink.setText("<a>Update Plugin</a>")
242+
updateLink.setLayoutData(gridData(verticalAlignment = SWT.BOTTOM, grabExcessVerticalSpace = true))
243+
244+
updateLink.addSelectionListener(object : SelectionAdapter() {
245+
override fun widgetSelected(e: SelectionEvent) {
246+
val job = updateOperation.getProvisioningJob(monitor)
247+
job.addJobChangeListener(RestartJobAdapter(display))
248+
job.schedule()
249+
250+
this@UpdatePluginNotification.close()
251+
}
252+
})
253+
}
254+
255+
override fun getPopupShellTitle(): String = "Kotlin Plugin"
256+
257+
private fun getInstallableUnit(): IInstallableUnit {
258+
return updateOperation.getProfileChangeRequest().getAdditions().first()
259+
}
260+
}
261+
262+
private fun gridData(
263+
horizontalAlignment: Int = SWT.BEGINNING,
264+
verticalAlignment: Int = SWT.CENTER,
265+
grabExcessHorizontalSpace: Boolean = false,
266+
grabExcessVerticalSpace: Boolean = false,
267+
horizontalSpan: Int = 1,
268+
verticalSpan: Int = 1): GridData {
269+
return GridData(horizontalAlignment, verticalAlignment, grabExcessHorizontalSpace, grabExcessVerticalSpace, horizontalSpan, verticalSpan)
270+
}
271+
272+
private class RestartJobAdapter(val display: Display) : JobChangeAdapter() {
273+
override fun done(event: IJobChangeEvent) {
274+
val result = event.result
275+
if (result.isOK) {
276+
display.syncExec {
277+
val restart = MessageDialog.openQuestion(
278+
null,
279+
"Updates installed, restart?",
280+
"Updates have been installed successfully, do you want to restart?");
281+
if (restart) {
282+
PlatformUI.getWorkbench().restart()
283+
}
284+
}
285+
} else if (!result.matches(IStatus.CANCEL)) {
286+
display.syncExec {
287+
MessageDialog.openError(null, "Error", event.result.message)
288+
}
289+
}
290+
}
291+
}

kotlin-eclipse-ui/src/org/jetbrains/kotlin/ui/builder/KotlinBuilder.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import org.jetbrains.kotlin.progress.ProgressIndicatorAndCompilationCanceledStat
4747
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
4848
import org.jetbrains.kotlin.progress.CompilationCanceledException
4949
import org.jetbrains.kotlin.core.asJava.KotlinLightClassGeneration
50+
import org.jetbrains.kotlin.ui.KotlinPluginUpdater
51+
import org.jetbrains.kotlin.eclipse.ui.utils.runJob
5052

5153
class KotlinBuilder : IncrementalProjectBuilder() {
5254
override fun build(kind: Int, args: Map<String, String>?, monitor: IProgressMonitor?): Array<IProject>? {
@@ -69,6 +71,11 @@ class KotlinBuilder : IncrementalProjectBuilder() {
6971

7072
if (affectedFiles.isNotEmpty()) {
7173
KotlinLightClassGeneration.updateLightClasses(javaProject, affectedFiles)
74+
75+
runJob("Checking for update", Job.DECORATE) {
76+
KotlinPluginUpdater.kotlinFileEdited()
77+
Status.OK_STATUS
78+
}
7279
}
7380

7481
val ktFiles = existingAffectedFiles.map { KotlinPsiManager.INSTANCE.getParsedFile(it) }

0 commit comments

Comments
 (0)