Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,19 @@ package com.intellij.plugin.applescript.lang.dictionary.discovery
import com.intellij.plugin.applescript.lang.dictionary.persistence.DictionaryInfo

/**
* Phase 4 SERVICE-09 (plan 04-04 / D-05): sealed return type for
* [com.intellij.plugin.applescript.lang.dictionary.files.SdefFileProvider.fetch].
* Typed result of one application-dictionary load attempt.
*
* Three variants give call sites compile-time exhaustive `when` and a typed channel for
* each fetch outcome:
* - [Empty] the application has no dictionary (legitimate state — e.g. non-scriptable
* app, or a name the discovery sweep could not resolve to a bundle file).
* - [Loaded] dictionary file was generated / cached AND a [DictionaryInfo] was populated.
* - [Failed] a recoverable error occurred during the load (sdef CLI missing, file
* permissions, malformed bundle, etc.). The `cause` carries the underlying
* exception when available.
*
* Service-INTERNAL scope only — this type does NOT appear on any `@JvmStatic` parser-util method
* and is NOT exposed on the parser hot path. PSI-side sealing for resolver / completion shapes is
* deferred until that surface is refactored.
* Three variants give call sites an exhaustive returned outcome:
* - [Empty] no application or supported dictionary input could be resolved.
* - [Loaded] a dictionary was cached, registered, and initialized.
* - [Failed] a load step failed or `fetch` converted an unexpected load error. The `cause`
* carries the underlying exception when available.
*/
sealed interface DictionaryLoadResult {
/** No dictionary available for the requested application. */
object Empty : DictionaryLoadResult

/** Dictionary successfully generated / cached and registered with persistence. */
/** Dictionary successfully cached, registered, and initialized. */
data class Loaded(
val info: DictionaryInfo,
) : DictionaryLoadResult
Expand All @@ -32,7 +24,7 @@ sealed interface DictionaryLoadResult {
* Dictionary fetch failed for [applicationName] with the given [reason]. [cause] is
* non-null when the failure originated from an exception (sdef CLI missing,
* NotScriptableApplicationException, IO error); null when the failure is a value-level
* predicate (e.g. unsupported file extension, off-EDT guard tripped).
* predicate (e.g. generation or initialization returned no usable output).
*/
data class Failed(
val applicationName: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ package com.intellij.plugin.applescript.lang.dictionary.files
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.SystemInfo
import com.intellij.plugin.applescript.lang.dictionary.discovery.ApplicationDiscoveryService
import com.intellij.plugin.applescript.lang.dictionary.discovery.DeveloperToolsNotInstalledException
import com.intellij.plugin.applescript.lang.dictionary.discovery.NotScriptableApplicationException
import com.intellij.plugin.applescript.lang.dictionary.discovery.XcodeDetectionService
import com.intellij.plugin.applescript.lang.dictionary.persistence.DictionaryInfo
import com.intellij.plugin.applescript.lang.dictionary.persistence.SdefPersistenceService
import com.intellij.plugin.applescript.lang.sdef.ApplicationDictionary
import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit
Expand All @@ -21,63 +17,60 @@ private const val DICTIONARY_GENERATION_TIMEOUT_SECONDS: Long = 5L
private val DICTIONARY_FILE_EXTENSIONS: Set<String> = setOf("xml", "sdef")

internal object SdefDictionaryFileGenerator {
fun createDictionaryInfoForApplication(
fun generateDictionaryFile(
applicationName: String,
applicationIoFile: File,
): DictionaryInfo? {
fun finishGeneration(
targetFile: File,
fileGenerated: Boolean,
) {
if (!fileGenerated) {
LOG.warn("Error occurred while generating file.")
if (targetFile.delete()) LOG.warn("Created file was deleted")
} else if (ApplicationDiscoveryService.getInstance().removeFromNotFoundList(applicationName)) {
LOG.debug("Application was removed from ignored list")
}
}

val appExtension = applicationIoFile.extension
val isDictionaryFile = appExtension in DICTIONARY_FILE_EXTENSIONS
generateCacheFile: (() -> Boolean)? = null,
): File? {
val isDictionaryFile = applicationIoFile.extension in DICTIONARY_FILE_EXTENSIONS
val serializePath = serializeDictionaryPathForApplication(applicationName)
val targetFile = File(serializePath)
val parentDirectoryReady = targetFile.parentFile.exists() || targetFile.parentFile.mkdirs()
if ((!SystemInfo.isMac && !isDictionaryFile) || !parentDirectoryReady) return null

LOG.debug("=== Caching Dictionary for application [$applicationName] ===")
var fileGenerated = false
var isGenerated = false
try {
fileGenerated =
generateDictionaryFileForApplication(
DictionaryGenerationRequest(
applicationName,
applicationIoFile,
targetFile,
serializePath,
isDictionaryFile,
),
)
} catch (e: NotScriptableApplicationException) {
LOG.warn("Generation failed: ${e.message}. Adding to ignore list")
service<SdefPersistenceService>().addNotScriptable(e.applicationName)
} catch (e: DeveloperToolsNotInstalledException) {
LOG.warn("Generation failed: ${e.message}")
fileGenerated =
recoverDictionaryFileFromBundledSdef(
applicationName,
applicationIoFile,
targetFile,
)
isGenerated =
generateCacheFile?.invoke()
?: writeCacheFile(
DictionaryGenerationRequest(
applicationName,
applicationIoFile,
targetFile,
serializePath,
isDictionaryFile,
),
)
} finally {
finishGeneration(targetFile, fileGenerated)
if (!isGenerated) {
LOG.warn("Error occurred while generating file.")
if (targetFile.delete()) LOG.warn("Created file was deleted")
}
}

return targetFile.takeIf { isGenerated && it.exists() }
}

fun recoverDictionaryFile(
applicationName: String,
applicationIoFile: File,
copyDictionaryFile: (String, File, File, Boolean) -> Boolean = ::copyDictionaryFileToCacheDir,
): File? {
LOG.warn("Will try to find application scripting definition file...")
val sdefFile = findSdefForApplication(applicationIoFile)
if (sdefFile == null || !sdefFile.exists()) {
LOG.warn("Scripting definition was not found for application ${applicationIoFile.absolutePath}")
return null
}
val targetFile = File(serializeDictionaryPathForApplication(applicationName))
if (!copyDictionaryFile(applicationName, sdefFile, targetFile, true)) {
throw IOException("Failed to cache bundled dictionary for $applicationName")
}
return targetFile
.takeIf { fileGenerated && it.exists() }
?.let { registerGeneratedDictionaryInfo(applicationName, it, applicationIoFile, appExtension) }
}

private fun generateDictionaryFileForApplication(request: DictionaryGenerationRequest): Boolean =
private fun writeCacheFile(request: DictionaryGenerationRequest): Boolean =
if (SystemInfo.isMac) {
val cmdName = if (request.isDictionaryFile) "cat" else "sdef"
doGenerateDictionaryFile(
Expand All @@ -95,94 +88,47 @@ internal object SdefDictionaryFileGenerator {
)
}

@Throws(NotScriptableApplicationException::class, DeveloperToolsNotInstalledException::class)
@Throws(
NotScriptableApplicationException::class,
DeveloperToolsNotInstalledException::class,
InterruptedException::class,
IOException::class,
)
private fun doGenerateDictionaryFile(
applicationName: String,
serializePath: String,
cmdName: String,
appFilePath: String,
): Boolean {
fun logFailure(cause: Throwable) {
LOG.error(
"Failed to create dictionary file for application [$applicationName] Command:$cmdName " +
"target path: $appFilePath Reason: ${cause.cause}",
cause,
)
}

var isGenerated = false
try {
val shellCommand = arrayOf("/bin/bash", "-c", " $cmdName \"$appFilePath\" > $serializePath")
LOG.debug("executing command: ${shellCommand.contentToString()}")
val execStart = System.currentTimeMillis()
val exitStatus =
Runtime
.getRuntime()
.exec(shellCommand)
.waitFor(DICTIONARY_GENERATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
val execEnd = System.currentTimeMillis()
if (!exitStatus) {
if (service<XcodeDetectionService>().isXcodeInstalled()) {
throw NotScriptableApplicationException(
applicationName,
"Waiting time elapsed for command ${shellCommand.contentToString()}. " +
"Seems that application \"$applicationName\" is not scriptable.",
)
} else {
throw DeveloperToolsNotInstalledException()
}
val shellCommand = arrayOf("/bin/bash", "-c", " $cmdName \"$appFilePath\" > $serializePath")
LOG.debug("executing command: ${shellCommand.contentToString()}")
val execStart = System.currentTimeMillis()
val isFinished =
Runtime
.getRuntime()
.exec(shellCommand)
.waitFor(DICTIONARY_GENERATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
val execEnd = System.currentTimeMillis()
if (!isFinished) {
if (service<XcodeDetectionService>().isXcodeInstalled()) {
throw NotScriptableApplicationException(
applicationName,
"Waiting time elapsed for command ${shellCommand.contentToString()}. " +
"Seems that application \"$applicationName\" is not scriptable.",
)
} else {
throw DeveloperToolsNotInstalledException()
}
LOG.debug("Waiting time elapsed. Execution time: ${execEnd - execStart} ms.")
isGenerated = true
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
logFailure(e)
} catch (e: IOException) {
logFailure(e)
}
return isGenerated
}

private fun recoverDictionaryFileFromBundledSdef(
applicationName: String,
applicationIoFile: File,
targetFile: File,
): Boolean {
LOG.warn("Will try to find application scripting definition file...")
val sdefFile = findSdefForApplication(applicationIoFile)
return if (sdefFile != null && sdefFile.exists()) {
copyDictionaryFileToCacheDir(applicationName, sdefFile, targetFile, true)
} else {
LOG.warn(
"Scripting definition was not found for application " +
applicationIoFile.absolutePath,
)
service<SdefPersistenceService>().addNotScriptable(applicationName)
false
}
LOG.debug("Waiting time elapsed. Execution time: ${execEnd - execStart} ms.")
return true
}

private fun findSdefForApplication(applicationIoFile: File): File? {
val appResources = File(applicationIoFile, "/Contents/Resources")
val files = appResources.listFiles { _, fileName -> fileName.endsWith("sdef") }
return if (!files.isNullOrEmpty()) files[0] else null
}

private fun registerGeneratedDictionaryInfo(
applicationName: String,
targetFile: File,
applicationIoFile: File,
appExtension: String,
): DictionaryInfo {
val applicationBundle =
applicationIoFile.takeIf {
ApplicationDictionary.SUPPORTED_APPLICATION_EXTENSIONS.contains(appExtension)
}
val dictionaryInfo = DictionaryInfo(applicationName, targetFile, applicationBundle)
service<SdefPersistenceService>().addDictionaryInfo(dictionaryInfo)
LOG.debug("Dictionary file generated for application [$applicationName]$targetFile")
return dictionaryInfo
}
}

private data class DictionaryGenerationRequest(
Expand Down
Loading
Loading