@@ -22,28 +22,21 @@ import com.microsoft.azure.toolkit.intellij.legacy.function.settings.AzureFuncti
22
22
import io.ktor.client.*
23
23
import io.ktor.client.call.*
24
24
import io.ktor.client.engine.cio.*
25
- import io.ktor.client.plugins.contentnegotiation.*
25
+ import io.ktor.client.plugins.HttpTimeout
26
26
import io.ktor.client.request.*
27
- import io.ktor.serialization.kotlinx.json.*
28
- import io.ktor.utils.io.ByteReadChannel
29
- import io.ktor.utils.io.core.isEmpty
30
- import io.ktor.utils.io.core.readBytes
27
+ import io.ktor.util.cio.*
28
+ import io.ktor.utils.io.*
31
29
import kotlinx.coroutines.Dispatchers
32
30
import kotlinx.coroutines.sync.Mutex
33
31
import kotlinx.coroutines.sync.withLock
34
32
import kotlinx.coroutines.withContext
35
33
import kotlinx.serialization.ExperimentalSerializationApi
36
34
import kotlinx.serialization.json.Json
35
+ import kotlinx.serialization.json.decodeFromStream
37
36
import java.io.File
38
37
import java.nio.file.Path
39
38
import java.nio.file.attribute.PosixFilePermission
40
- import kotlin.io.path.ExperimentalPathApi
41
- import kotlin.io.path.Path
42
- import kotlin.io.path.createDirectories
43
- import kotlin.io.path.deleteRecursively
44
- import kotlin.io.path.exists
45
- import kotlin.io.path.isExecutable
46
- import kotlin.io.path.setPosixFilePermissions
39
+ import kotlin.io.path.*
47
40
48
41
@Service(Service .Level .APP )
49
42
class FunctionsToolingFeedService : Disposable {
@@ -64,29 +57,86 @@ class FunctionsToolingFeedService : Disposable {
64
57
trustManager = CertificateManager .getInstance().trustManager
65
58
}
66
59
}
67
- install(ContentNegotiation ) {
68
- json(Json {
69
- explicitNulls = false
70
- ignoreUnknownKeys = true
71
- allowTrailingComma = true
72
- })
60
+ install(HttpTimeout ) {
61
+ requestTimeoutMillis = 300000
73
62
}
74
63
}
75
64
65
+ @OptIn(ExperimentalSerializationApi ::class )
66
+ private val json = Json {
67
+ explicitNulls = false
68
+ ignoreUnknownKeys = true
69
+ allowTrailingComma = true
70
+ allowComments = true
71
+ }
72
+
73
+ /* *
74
+ * Downloads the latest Azure Functions tooling release for the specified Azure Functions runtime version.
75
+ *
76
+ * This method fetches the release information, determines if the release has already been downloaded,
77
+ * and if not, it downloads the release, extracts it to the appropriate directory and cleans up any temporary files.
78
+ *
79
+ * @param functionsRuntimeVersion The version of Azure Functions runtime for which to download the latest tooling release.
80
+ * @return A Result wrapping the path to the latest Azure Functions tooling release.
81
+ */
82
+ suspend fun downloadLatestFunctionsToolingRelease (functionsRuntimeVersion : String ): Result <Path > {
83
+ downloadAndSaveReleaseFeed().onFailure { error ->
84
+ LOG .warn(" Unable to download Function tooling release feed" , error)
85
+ return Result .failure(error)
86
+ }
87
+
88
+ val toolingRelease = getLatestFunctionsToolingRelease(functionsRuntimeVersion)
89
+ ? : return Result .failure(IllegalStateException (" Unable to obtain latest function tooling release" ))
90
+ val toolingReleasePath = getPathForLatestFunctionsToolingRelease(toolingRelease)
91
+ ? : return Result .failure(IllegalStateException (" Unable to path to download function tooling release" ))
92
+
93
+ val coreToolsExecutablePath = toolingReleasePath.resolveFunctionCoreToolsExecutable()
94
+ if (coreToolsExecutablePath.exists()) {
95
+ LOG .trace { " The release $toolingRelease is already downloaded" }
96
+ return Result .success(toolingReleasePath)
97
+ }
98
+
99
+ return downloadAndExtractFunctionsToolingRelease(
100
+ toolingRelease,
101
+ toolingReleasePath,
102
+ coreToolsExecutablePath
103
+ )
104
+ }
105
+
106
+ /* *
107
+ * Retrieves a list of Azure Functions tooling releases for the specified Azure Functions runtime versions.
108
+ *
109
+ * @param functionsRuntimeVersions List of Azure Functions runtime versions.
110
+ * @return List of tooling releases corresponding to the given versions, or null if the release feed could not be downloaded.
111
+ */
112
+ suspend fun getFunctionsToolingReleaseForVersions (functionsRuntimeVersions : List <String >): List <FunctionsToolingRelease >? {
113
+ downloadAndSaveReleaseFeed().onFailure { exception ->
114
+ LOG .warn(" Unable to download Function tooling release feed" , exception)
115
+ return null
116
+ }
117
+
118
+ return functionsRuntimeVersions.mapNotNull { getLatestFunctionsToolingRelease(it) }
119
+ }
120
+
76
121
/* *
77
122
* Downloads and saves the Azure Functions tooling release feed if the release cache is empty.
78
123
*
79
124
* @return Result wrapping any exception encountered during the execution.
80
125
*/
81
- suspend fun downloadAndSaveReleaseFeed () = kotlin.runCatching {
82
- if (releaseCache.isNotEmpty()) return @runCatching
126
+ private suspend fun downloadAndSaveReleaseFeed (): Result < Unit > {
127
+ if (releaseCache.isNotEmpty()) return Result .success( Unit )
83
128
84
129
releaseCacheMutex.withLock {
85
- if (releaseCache.isNotEmpty()) return @withLock
130
+ if (releaseCache.isNotEmpty()) return Result .success( Unit )
86
131
87
132
LOG .trace(" Downloading Functions tooling release feed" )
88
133
89
- val feed = getReleaseFeed()
134
+ val feedResult = downloadFunctionsToolingReleaseFeed().onFailure { error ->
135
+ return Result .failure(error)
136
+ }
137
+
138
+ val feed = feedResult.getOrNull()
139
+ ? : return Result .failure(IllegalStateException (" Unable to download Function tooling release feed" ))
90
140
val releaseTags = feed.tags
91
141
.toSortedMap()
92
142
.filterValues { ! it.releaseQuality.isNullOrEmpty() && ! it.release.isNullOrEmpty() && ! it.hidden }
@@ -105,93 +155,74 @@ class FunctionsToolingFeedService : Disposable {
105
155
FunctionsToolingRelease (releaseKey, releaseFromTag, coreToolsRelease.downloadLink ? : " " )
106
156
)
107
157
}
158
+
159
+ return Result .success(Unit )
108
160
}
109
161
}
110
162
111
- /* *
112
- * Downloads the latest Azure Functions tooling release for the specified Azure Functions runtime version.
113
- *
114
- * This method fetches the release information, determines if the release has already been downloaded,
115
- * and if not, it downloads the release, extracts it to the appropriate directory and cleans up any temporary files.
116
- *
117
- * @param functionsRuntimeVersion The version of Azure Functions runtime for which to download the latest tooling release.
118
- * @return A Result wrapping the path to the latest Azure Functions tooling release.
119
- */
120
- suspend fun downloadLatestFunctionsToolingRelease (functionsRuntimeVersion : String ): Result <Path > {
121
- downloadAndSaveReleaseFeed().onFailure { exception ->
122
- LOG .warn(" Unable to download Function tooling release feed" , exception)
123
- return Result .failure(exception)
124
- }
163
+ private suspend fun downloadFunctionsToolingReleaseFeed (): Result <ReleaseFeed > = runCatching {
164
+ val feedUrl = Registry .get(" azure.function_app.core_tools.feed.url" ).asString()
165
+ LOG .trace { " Functions tooling release feed: $feedUrl " }
125
166
126
- val toolingRelease = getLatestFunctionsToolingRelease(functionsRuntimeVersion)
127
- if (toolingRelease == null ) {
128
- return Result .failure(IllegalStateException (" Unable to obtain latest function tooling release" ))
129
- }
130
- val toolingReleasePath = getPathForLatestFunctionsToolingRelease(toolingRelease)
131
- if (toolingReleasePath == null ) {
132
- return Result .failure(IllegalStateException (" Unable to path to download function tooling release" ))
167
+ val temporaryFeedFile = FileUtil .createTempFile(
168
+ File (FileUtil .getTempDirectory()),
169
+ " AzureFunctionsToolingFeed" ,
170
+ " .json" ,
171
+ true ,
172
+ false
173
+ )
174
+ val temporaryFeedPath = temporaryFeedFile.toPath()
175
+
176
+ LOG .trace { " Created a temporary feed file: ${temporaryFeedPath.absolutePathString()} " }
177
+
178
+ withContext(Dispatchers .IO ) {
179
+ client.prepareGet(feedUrl).execute { httpResponse ->
180
+ val channel: ByteReadChannel = httpResponse.body()
181
+ channel.copyAndClose(temporaryFeedFile.writeChannel())
182
+ }
133
183
}
134
- val coreToolsExecutablePath = toolingReleasePath.resolveFunctionCoreToolsExecutable()
135
- if (coreToolsExecutablePath.exists()) {
136
- LOG .trace { " The release $toolingRelease is already downloaded" }
137
- return Result .success(toolingReleasePath)
184
+
185
+ LOG .trace { " Downloaded Functions tooling feed to the ${temporaryFeedPath.absolutePathString()} " }
186
+
187
+ val feed = withContext(Dispatchers .IO ) {
188
+ json.decodeFromStream<ReleaseFeed >(temporaryFeedPath.inputStream())
138
189
}
139
190
140
- return downloadAndExtractFunctionsToolingRelease(
141
- toolingRelease,
142
- toolingReleasePath,
143
- coreToolsExecutablePath
144
- )
191
+ temporaryFeedPath.deleteIfExists()
192
+
193
+ return Result .success(feed)
145
194
}
146
195
196
+
147
197
private suspend fun downloadAndExtractFunctionsToolingRelease (
148
198
toolingRelease : FunctionsToolingRelease ,
149
199
toolingReleasePath : Path ,
150
200
coreToolsExecutablePath : Path ,
151
201
): Result <Path > {
202
+ if (coreToolsExecutablePath.exists()) {
203
+ LOG .trace { " The release $toolingRelease is already downloaded" }
204
+ return Result .success(toolingReleasePath)
205
+ }
206
+
152
207
functionsToolingReleaseMutex.withLock {
153
208
if (coreToolsExecutablePath.exists()) {
154
209
LOG .trace { " The release $toolingRelease is already downloaded" }
155
210
return Result .success(toolingReleasePath)
156
211
}
157
212
158
213
try {
159
- val tempFile = FileUtil .createTempFile(
160
- File (FileUtil .getTempDirectory()),
161
- " AzureFunctions-${toolingRelease.functionsVersion} -${toolingRelease.releaseTag} " ,
162
- " .zip" ,
163
- true ,
164
- true
165
- )
166
-
167
- LOG .trace { " Created a temporary file: ${tempFile.absolutePath} " }
168
-
169
- withContext(Dispatchers .IO ) {
170
- client.prepareGet(toolingRelease.artifactUrl).execute { httpResponse ->
171
- val channel: ByteReadChannel = httpResponse.body()
172
- while (! channel.isClosedForRead) {
173
- val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE .toLong())
174
- while (! packet.isEmpty) {
175
- val bytes = packet.readBytes()
176
- tempFile.appendBytes(bytes)
177
- }
178
- }
179
- }
180
- }
214
+ val temporaryArchive = downloadFunctionsToolingArchive(toolingRelease)
181
215
182
- LOG .trace { " Downloaded core tooling archive to the ${tempFile.absolutePath} " }
216
+ if ( ! toolingReleasePath.exists()) toolingReleasePath.createDirectories()
183
217
184
- if ( ! toolingReleasePath.exists())
185
- toolingReleasePath.createDirectories( )
218
+ LOG .trace { " Extracting from ${temporaryArchive.absolutePathString()} to $toolingReleasePath " }
219
+ ZipUtil .extract(temporaryArchive, toolingReleasePath, null , true )
186
220
187
- LOG .trace { " Extracting from ${tempFile.absolutePath} to $toolingReleasePath " }
188
- ZipUtil .extract(tempFile.toPath(), toolingReleasePath, null , true )
221
+ temporaryArchive.deleteIfExists()
189
222
190
- if (tempFile.exists())
191
- tempFile.delete()
192
-
193
- if (! coreToolsExecutablePath.isExecutable() && ! SystemInfo .isWindows)
223
+ if (! coreToolsExecutablePath.isExecutable() && ! SystemInfo .isWindows) {
194
224
setExecutablePermissionsForCoreTools(coreToolsExecutablePath)
225
+ }
195
226
196
227
return Result .success(toolingReleasePath)
197
228
} catch (e: Exception ) {
@@ -202,30 +233,28 @@ class FunctionsToolingFeedService : Disposable {
202
233
}
203
234
}
204
235
205
- /* *
206
- * Retrieves a list of Azure Functions tooling releases for the specified Azure Functions runtime versions.
207
- *
208
- * @param functionsRuntimeVersions List of Azure Functions runtime versions.
209
- * @return List of tooling releases corresponding to the given versions, or null if the release feed could not be downloaded.
210
- */
211
- suspend fun getFunctionsToolingReleaseForVersions (functionsRuntimeVersions : List <String >): List <FunctionsToolingRelease >? {
212
- downloadAndSaveReleaseFeed().onFailure { exception ->
213
- LOG .warn(" Unable to download Function tooling release feed" , exception)
214
- return null
215
- }
216
-
217
- return functionsRuntimeVersions.mapNotNull { getLatestFunctionsToolingRelease(it) }
218
- }
236
+ private suspend fun downloadFunctionsToolingArchive (toolingRelease : FunctionsToolingRelease ): Path {
237
+ val temporaryArchive = FileUtil .createTempFile(
238
+ File (FileUtil .getTempDirectory()),
239
+ " AzureFunctions-${toolingRelease.functionsVersion} -${toolingRelease.releaseTag} " ,
240
+ " .zip" ,
241
+ true ,
242
+ false
243
+ )
244
+ val temporaryArchivePath = temporaryArchive.toPath()
219
245
220
- private suspend fun getReleaseFeed (): ReleaseFeed {
221
- val feedUrl = Registry .get(" azure.function_app.core_tools.feed.url" ).asString()
222
- LOG .trace { " Functions tooling release feed: $feedUrl " }
246
+ LOG .trace { " Created a temporary file: ${temporaryArchivePath.absolutePathString()} " }
223
247
224
- val response = withContext(Dispatchers .IO ) {
225
- client.get(feedUrl)
248
+ withContext(Dispatchers .IO ) {
249
+ client.prepareGet(toolingRelease.artifactUrl).execute { httpResponse ->
250
+ val channel: ByteReadChannel = httpResponse.body()
251
+ channel.copyAndClose(temporaryArchive.writeChannel())
252
+ }
226
253
}
227
254
228
- return response.body<ReleaseFeed >()
255
+ LOG .trace { " Downloaded Functions tooling archive to the ${temporaryArchivePath.absolutePathString()} " }
256
+
257
+ return temporaryArchivePath
229
258
}
230
259
231
260
private fun getLatestFunctionsToolingRelease (functionsRuntimeVersion : String ): FunctionsToolingRelease ? {
@@ -284,4 +313,4 @@ class FunctionsToolingFeedService : Disposable {
284
313
}
285
314
286
315
override fun dispose () = client.close()
287
- }
316
+ }
0 commit comments