diff --git a/CHANGELOG.md b/CHANGELOG.md
index fbf68cdcc..cba9a89bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0).
This plugin has functionality that is common to both ReSharper and Rider. It also contains a plugin for the Unity editor that is used to communicate with Rider. Changes marked with a "Rider:" prefix are specific to Rider, while changes for the Unity editor plugin are marked with a "Unity editor:" prefix. No prefix means that the change is common to both Rider and ReSharper.
## 2019.3.1
-* [Commits](https://github.com/JetBrains/resharper-unity/compare/net193-eap7-rtm-2019.3.0...net193)
+* [Commits](https://github.com/JetBrains/resharper-unity/compare/net193-eap7-rtm-2019.3.0...net193-eap7-rtm-2019.3.0-rtm-2019.3.1)
* [Milestone](https://github.com/JetBrains/resharper-unity/milestone/33?closed=1)
### Added
@@ -17,10 +17,19 @@ This plugin has functionality that is common to both ReSharper and Rider. It als
### Changed
- Rider: Entire plugin is no longer disabled if the CSS plugin is disabled ([RIDER-36523](https://youtrack.jetbrains.com/issue/RIDER-36523), [#1443](https://github.com/JetBrains/resharper-unity/pull/1443))
+- Rider: Make Attach to Unity Process dialog resizable ([#1446](https://github.com/JetBrains/resharper-unity/issues/1446), [#1450](https://github.com/JetBrains/resharper-unity/pull/1450))
+- Rider: Identify child processes by role in Attach to Unity Process dialog ([#1328](https://github.com/JetBrains/resharper-unity/issues/1328), [#1450](https://github.com/JetBrains/resharper-unity/pull/1450))
### Fixed
- Fix usage count for custom event based event handlers in Unity 2018.4+ ([#1448](https://github.com/JetBrains/resharper-unity/issues/1448), [#1449](https://github.com/JetBrains/resharper-unity/pull/1449))
+- Rider: Show correct project name when Unity started with certain command line on Windows ([#1450](https://github.com/JetBrains/resharper-unity/pull/1450))
+- Rider: Show correct project name when multiple Unity processes listed in Attach to Process popup list ([#1456](https://github.com/JetBrains/resharper-unity/issues/1456), [#1450](https://github.com/JetBrains/resharper-unity/pull/1450))
+- Rider: Fix exception in Attach to Unity Process dialog causing list to be empty ([#1454](https://github.com/JetBrains/resharper-unity/issues/1454), [#1450](https://github.com/JetBrains/resharper-unity/pull/1450))
+- Rider: Show run configuration dialog for Unity class library projects ([#1445](https://github.com/JetBrains/resharper-unity/issues/1445), [#1450](https://github.com/JetBrains/resharper-unity/pull/1450))
+- Rider: Fix finding existing Unity instance to debug ([RIDER-36256](https://youtrack.jetbrains.com/issue/RIDER-36256), [#1450](https://github.com/JetBrains/resharper-unity/pull/1450))
+- Rider: Fix `EditorInstance.json` being locked by Rider ([#1450](https://github.com/JetBrains/resharper-unity/pull/1450))
+
## 2019.3
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt b/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt
index 6bad0c45a..fabe86f19 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt
@@ -1,6 +1,7 @@
package com.jetbrains.rider
import com.intellij.openapi.project.Project
+import com.jetbrains.rd.util.reactive.valueOrDefault
import com.jetbrains.rdclient.util.idea.LifetimedProjectComponent
import com.jetbrains.rider.model.RdExistingSolution
import com.jetbrains.rider.plugins.unity.UnityHost
@@ -21,6 +22,13 @@ class UnityProjectDiscoverer(project: Project, unityHost: UnityHost) : Lifetimed
val isUnityGeneratedProject = isUnityProject && solutionNameMatchesUnityProjectName(project)
val isUnitySidecarProject = isUnityProject && !solutionNameMatchesUnityProjectName(project)
+ // Note that this will only return a sensible value once the solution + backend have finished loading
+ val isUnityClassLibraryProject: Boolean?
+ get() {
+ val hasReference = hasUnityReference.valueOrNull ?: return null
+ return hasReference && isCorrectlyLoadedSolution(project)
+ }
+
companion object {
fun getInstance(project: Project) = project.getComponent()
@@ -58,5 +66,6 @@ class UnityProjectDiscoverer(project: Project, unityHost: UnityHost) : Lifetimed
}
fun Project.isUnityGeneratedProject() = UnityProjectDiscoverer.getInstance(this).isUnityGeneratedProject
+fun Project.isUnityClassLibraryProject() = UnityProjectDiscoverer.getInstance(this).isUnityClassLibraryProject
fun Project.isUnityProject()= UnityProjectDiscoverer.getInstance(this).isUnityProject
fun Project.isUnityProjectFolder()= UnityProjectDiscoverer.getInstance(this).isUnityProjectFolder
\ No newline at end of file
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityPlayer.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityPlayer.kt
index 4f6476f9d..c1ba25014 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityPlayer.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityPlayer.kt
@@ -3,11 +3,12 @@ package com.jetbrains.rider.plugins.unity.run
data class UnityPlayer(val host: String, val port: Int, val debuggerPort: Int,
val flags: Long, val guid: Long, val editorId: Long, val version: Int,
val id: String, val allowDebugging: Boolean, val packageName: String? = null,
- val projectName: String? = null, val pid: Int? = null, val isEditor: Boolean = false) {
+ val projectName: String? = null, val pid: Int? = null, val roleName: String? = null,
+ val isEditor: Boolean = false) {
companion object {
- fun createEditorPlayer(host: String, port: Int, id: String, pid: Int, projectName: String?): UnityPlayer {
+ fun createEditorPlayer(host: String, port: Int, id: String, pid: Int, projectName: String?, roleName: String?): UnityPlayer {
return UnityPlayer(host, port, port, flags = 0, guid = port.toLong(), editorId = port.toLong(), version = 0,
- id = id, allowDebugging = true, pid = pid, projectName = projectName, isEditor = true)
+ id = id, allowDebugging = true, pid = pid, projectName = projectName, isEditor = true, roleName = roleName)
}
fun createRemotePlayer(host: String, port: Int): UnityPlayer {
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityPlayerListener.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityPlayerListener.kt
index a663e76fe..a20c7db3a 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityPlayerListener.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityPlayerListener.kt
@@ -102,11 +102,12 @@ class UnityPlayerListener(private val project: Project,
private fun addLocalProcesses() {
val unityProcesses = OSProcessUtil.getProcessList().filter { UnityRunUtil.isUnityEditorProcess(it) }
- val projectNames = UnityRunUtil.getUnityProcessProjectNames(unityProcesses, project)
+ val unityProcessInfoMap = UnityRunUtil.getAllUnityProcessInfo(unityProcesses, project)
unityProcesses.map { processInfo ->
- val projectName = projectNames[processInfo.pid]
+ val unityProcessInfo = unityProcessInfoMap[processInfo.pid]
val port = convertPidToDebuggerPort(processInfo.pid)
- UnityPlayer.createEditorPlayer("127.0.0.1", port, processInfo.executableName, processInfo.pid, projectName)
+ UnityPlayer.createEditorPlayer("127.0.0.1", port, processInfo.executableName, processInfo.pid,
+ unityProcessInfo?.projectName, unityProcessInfo?.roleName)
}.forEach {
onPlayerAdded(it)
}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityProcessPickerDialog.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityProcessPickerDialog.kt
index decf46cf7..fe1013cad 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityProcessPickerDialog.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityProcessPickerDialog.kt
@@ -62,12 +62,12 @@ class UnityProcessPickerDialog(private val project: Project) : DialogWrapper(pro
}
commentRow("Please ensure both the Development Build and Script Debugging options are checked in Unity's Build Settings dialog. " +
"Standalone players must be visible to the current network.")
- }.apply { minimumSize = Dimension(650, 300) }
+ }.apply { preferredSize = Dimension(650, 450) }
isOKActionEnabled = false
cancelAction.putValue(FOCUSED_ACTION, true)
init()
- setResizable(false)
+ setResizable(true)
}
// DialogWrapper only lets the Mac set the preferred component via FOCUSED_ACTION because reasons
@@ -159,6 +159,9 @@ class UnityProcessPickerDialog(private val project: Project) : DialogWrapper(pro
val debug = player.allowDebugging && !UnityRunUtil.isDebuggerAttached(player.host, player.port, project)
val attributes = if (debug) SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES else SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES
append(player.id, attributes)
+ if (player.roleName != null) {
+ append(" ${player.roleName}", attributes)
+ }
if (player.projectName != null) {
append(" - ${player.projectName}", attributes)
}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityRunUtil.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityRunUtil.kt
index ea6a500ec..2ed58d76b 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityRunUtil.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/UnityRunUtil.kt
@@ -23,6 +23,8 @@ import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Paths
+data class UnityProcessInfo(val projectName: String?, val roleName: String?)
+
object UnityRunUtil {
private val logger = Logger.getInstance(UnityRunUtil::class.java)
@@ -59,12 +61,15 @@ object UnityRunUtil {
return processList.any { it.pid == pid && isUnityEditorProcess(it) }
}
- fun getUnityProcessProjectName(processInfo: ProcessInfo, project: Project): String? {
- return getUnityProcessProjectNames(listOf(processInfo), project)[processInfo.pid]
+ fun getUnityProcessInfo(processInfo: ProcessInfo, project: Project): UnityProcessInfo? {
+ return getAllUnityProcessInfo(listOf(processInfo), project)[processInfo.pid]
}
- fun getUnityProcessProjectNames(processList: List, project: Project): Map {
- // We have several options to get the project name (and directory, for future use):
+ fun getAllUnityProcessInfo(processList: List, project: Project): Map {
+ // We might have to call external processes. Make sure we're running in the background
+ assertNotDispatchThread()
+
+ // We have several options to get the project name (and maybe directory, for future use):
// 1) Match pid with EditorInstance.json - it's the current project
// 2) If the editor was started from Unity Hub, use the -projectPath or -createProject parameters
// 3) If we're on Mac/Linux, use `lsof -a -p {pid},{pid},{pid} -d cwd -Fn` to get the current working directory
@@ -73,22 +78,24 @@ object UnityRunUtil {
// E.g. https://stackoverflow.com/questions/16110936/read-other-process-current-directory-in-c-sharp
// 4) Scrape the main window title. This is fragile, as the format changes, and can easily break with hyphens in
// project or scene names. It also doesn't give us the project path. And it doesn't work on Mac/Linux
-
- // We might have to call external processes. Make sure we're running in the background
- assertNotDispatchThread()
- val projectNames = mutableMapOf()
+ val processInfoMap = mutableMapOf()
processList.forEach {
- val projectName = getProjectNameFromEditorInstanceJson(it, project) ?:
- getProjectNameFromCommandLine(it)
- projectName?.let { name -> projectNames[it.pid] = name }
+ try {
+ val projectName = getProjectNameFromEditorInstanceJson(it, project)
+ parseProcessInfoFromCommandLine(it, projectName)?.let { n -> processInfoMap[it.pid] = n }
+ }
+ catch (t: Throwable) {
+ logger.warn("Error fetching Unity process info: ${it.commandLine}", t)
+ }
}
- if (projectNames.size != processList.size) {
- fillProjectNamesFromWorkingDirectory(processList, projectNames)
+ // If we failed to get project name from the command line, try and get it from the working directory
+ if (processInfoMap.size != processList.size || processInfoMap.any { it.value.projectName == null }) {
+ fillProjectNamesFromWorkingDirectory(processList, processInfoMap)
}
- return projectNames
+ return processInfoMap
}
private fun assertNotDispatchThread() {
@@ -107,63 +114,102 @@ object UnityRunUtil {
} else null
}
- private fun getProjectNameFromCommandLine(processInfo: ProcessInfo): String? {
- // Make sure the command line we're using is properly quoted, if possible
- getQuotedCommandLine(processInfo)?.let {
- val tokenizer = CommandLineTokenizer(processInfo.commandLine)
- while (tokenizer.hasMoreTokens()) {
- val token = tokenizer.nextToken()
- if (token.equals("-projectPath", true) || token.equals("-createPath", true)) {
- return getProjectNameFromPath(StringUtil.unescapeStringCharacters(tokenizer.nextToken()))
- }
- }
- }
+ private fun parseProcessInfoFromCommandLine(processInfo: ProcessInfo, canonicalProjectName: String?): UnityProcessInfo? {
+ var projectName = canonicalProjectName
+ var name: String? = null
+ var umpProcessRole: String? = null
+ var umpWindowTitle: String? = null
- // Try to parse the unquoted command line, coping with a -projectPath or -createPath that might contain spaces
- // and/or hyphens. Split the command line at argument boundaries, e.g. a hyphen followed by a non-whitespace
- // char, with leading whitespace, or at the start of the string. Each split string should be an arg and an
- // argvalue (lookahead means we keep the delimiter). If the path contains an embedded space-hyphen-nonspace
- // sequence, we need to join segments until we're sure we've got the longest possible path.
- // There is a pathological edge case of two directories with the same name but one with a suffix that matches
- // the next command line argument. If we take the longest path, we can get the wrong one. E.g.
- // "/home/Unity/project1" and "/home/Unity/project1 -useHub" would confuse things. I think this is so unlikely
- // as to be happily ignored
- // This assumes that all arguments begin with a hyphen, and there are no standalone arguments. Empirically, this
- // is true
- val commandLineArgs = processInfo.commandLine.split("(^|\\s)(?=-[^\\s])".toRegex())
+ val tokens = tokenizeCommandLine(processInfo)
var i = 0
- do {
- if (commandLineArgs[i].startsWith("-projectPath", ignoreCase = true)
- || commandLineArgs[i].startsWith("-createProject", ignoreCase = true)) {
- val whitespace = commandLineArgs[i].indexOf(' ')
- if (whitespace == -1) continue // Weird if true
- var path = commandLineArgs[i].substring(whitespace + 1)
+ while (i < tokens.size - 1) { // -1 for the argument + argument value
+ val token = tokens[i++]
+ if (projectName == null && (token.equals("-projectPath", true) || token.equals("-createProject", true))) {
+ // For an unquoted command line, the next token isn't guaranteed to be the whole path. If the path
+ // contains a space-hyphen-char (e.g. `-projectPath /Users/matt/Projects/space game -is great -yeah`)
+ // they will be split as multiple tokens. Concatenate subsequent tokens until we have the longest valid
+ // path. Note that the arguments and values are all separated by a single space. Any other whitespace
+ // is still part of the string
+ var path = tokens[i++]
var lastValid = if (File(path).isDirectory) path else ""
- while (i < commandLineArgs.size - 1) {
- path += " " + commandLineArgs[++i]
+ var j = i
+ while (j < tokens.size) {
+ path += " " + tokens[j++]
if (File(path).isDirectory) {
lastValid = path
+ i = j
}
}
- return getProjectNameFromPath(lastValid)
+ projectName = getProjectNameFromPath(StringUtil.unquoteString(lastValid))
+ }
+ else if (token.equals("-name", true)) {
+ name = StringUtil.unquoteString(tokens[i++])
+ }
+ else if (token.equals("-ump-process-role", true)) {
+ umpProcessRole = StringUtil.unquoteString(tokens[i++])
}
+ else if (token.equals("-ump-window-title", true)) {
+ umpWindowTitle = StringUtil.unquoteString(tokens[i++])
+ }
+ }
+
+ if (projectName == null && name == null && umpWindowTitle == null && umpProcessRole == null) {
+ return null
+ }
+
+ return UnityProcessInfo(projectName, name ?: umpWindowTitle ?: umpProcessRole)
+ }
+
+ private fun tokenizeCommandLine(processInfo: ProcessInfo): List {
+ return tokenizeQuotedCommandLine(processInfo)
+ ?: tokenizeUnquotedCommandLine(processInfo)
+ }
- i++
- } while (i < commandLineArgs.size)
+ private fun tokenizeQuotedCommandLine(processInfo: ProcessInfo): List? {
+ return getQuotedCommandLine(processInfo)?.let {
+ val tokens = mutableListOf()
+ val tokenizer = CommandLineTokenizer(it)
+ while(tokenizer.hasMoreTokens())
+ tokens.add(tokenizer.nextToken())
+ tokens
+ }
+ }
- return null
+ private fun tokenizeUnquotedCommandLine(processInfo: ProcessInfo): List {
+ // Split the command line into arguments
+ // We assume an argument starts with a hyphen and has no whitespace in the name. Empirically, this is true
+ // So split on ^- or \s-
+ // Each chunk should now be an arg and an argvalue, e.g. `-name Foo`
+ // Split on the first whitespace. The argument value should be correct, but might require concatenating if
+ // the value pathologically contains a \s-[^\s] sequence
+ // E.g. `-createProject /Users/matt/my interesting -project` would split into the following tokens:
+ // "-createProject" "/Users/matt/my interesting" "-project"
+ // The single whitespace between arguments and between the argument and value is not captured, so must be added
+ // back if concatenating
+ val tokens = mutableListOf()
+ processInfo.commandLine.split("(^|\\s)(?=-[^\\s])".toRegex()).forEach {
+ val whitespace = it.indexOf(' ')
+ if (whitespace == -1) {
+ tokens.add(it)
+ }
+ else {
+ tokens.add(it.substring(0, whitespace))
+ tokens.add(it.substring(whitespace + 1))
+ }
+ }
+ return tokens
}
private fun getQuotedCommandLine(processInfo: ProcessInfo): String? {
return when {
- SystemInfo.isWindows -> processInfo.commandLine
- SystemInfo.isMac -> null
+ SystemInfo.isWindows -> processInfo.commandLine // Already quoted correctly
+ SystemInfo.isMac -> null // We can't add quotes, and can't easily get an unquoted version
SystemInfo.isUnix -> {
try {
// ProcessListUtil.getProcessListOnUnix already reads /proc/{pid}/cmdline, but doesn't quote
- // arguments that contain spaces, which makes it much harder to parse
+ // arguments that contain spaces. https://youtrack.jetbrains.com/issue/IDEA-229022
val procfsCmdline = File("/proc/${processInfo.pid}/cmdline")
val cmdlineString = String(FileUtil.loadFileBytes(procfsCmdline), StandardCharsets.UTF_8)
val cmdlineParts = StringUtil.split(cmdlineString, "\u0000")
@@ -193,8 +239,10 @@ object UnityRunUtil {
private fun getProjectNameFromPath(projectPath: String): String = Paths.get(projectPath).fileName.toString()
- private fun fillProjectNamesFromWorkingDirectory(processList: List, projectNames: MutableMap) {
+ private fun fillProjectNamesFromWorkingDirectory(processList: List, projectNames: MutableMap) {
+ // Windows requires reading process memory. Unix is so much nicer.
if (SystemInfo.isWindows) return
+
try {
val processIds = processList.joinToString(",") { it.pid.toString() }
val command = when {
@@ -212,7 +260,7 @@ object UnityRunUtil {
val pid = stdout[i].substring(1).toInt()
val cwd = getProjectNameFromPath(stdout[i + 2].substring(1))
- projectNames[pid] = cwd
+ projectNames[pid] = UnityProcessInfo(cwd, projectNames[pid]?.roleName)
}
}
}
@@ -245,4 +293,4 @@ object UnityRunUtil {
.build()
ProgramRunnerUtil.executeConfiguration(environment, false, true)
}
-}
\ No newline at end of file
+}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/attach/UnityLocalAttachProcessDebuggerProvider.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/attach/UnityLocalAttachProcessDebuggerProvider.kt
index 8679f98c5..8c02eb667 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/attach/UnityLocalAttachProcessDebuggerProvider.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/attach/UnityLocalAttachProcessDebuggerProvider.kt
@@ -5,20 +5,24 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder
import com.intellij.xdebugger.attach.*
+import com.jetbrains.rdclient.util.idea.getOrCreateUserData
+import com.jetbrains.rider.plugins.unity.run.UnityProcessInfo
import com.jetbrains.rider.plugins.unity.run.UnityRunUtil
@Suppress("UnstableApiUsage")
class UnityLocalAttachProcessDebuggerProvider : XAttachDebuggerProvider {
companion object {
- val PROJECT_NAME_KEY: Key = Key("UnityProcess::ProjectName")
+ val PROCESS_INFO_KEY: Key> = Key("UnityProcess::Info")
}
override fun getAvailableDebuggers(project: Project, host: XAttachHost, process: ProcessInfo, userData: UserDataHolder): MutableList {
if (UnityRunUtil.isUnityEditorProcess(process)) {
- // Cache the project name. When we're asked for display name, we're on the EDT thread, and can't call this
- UnityRunUtil.getUnityProcessProjectName(process, project)?.let {
- userData.putUserData(PROJECT_NAME_KEY, it)
+ // Cache the processes display names. When we're asked for the display text for the menu, we're on the EDT
+ // thread, and can't call this
+ UnityRunUtil.getUnityProcessInfo(process, project)?.let {
+ val map = userData.getOrCreateUserData(PROCESS_INFO_KEY) { mutableMapOf() }
+ map[process.pid]= it
}
return mutableListOf(UnityLocalAttachDebugger())
}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/attach/UnityLocalAttachProcessPresentationGroup.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/attach/UnityLocalAttachProcessPresentationGroup.kt
index 00707685f..e6383ea30 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/attach/UnityLocalAttachProcessPresentationGroup.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/attach/UnityLocalAttachProcessPresentationGroup.kt
@@ -14,13 +14,10 @@ object UnityLocalAttachProcessPresentationGroup : XAttachProcessPresentationGrou
override fun getItemIcon(project: Project, process: ProcessInfo, userData: UserDataHolder) = UnityIcons.Icons.UnityLogo
override fun getItemDisplayText(project: Project, process: ProcessInfo, userData: UserDataHolder): String {
- val projectName = userData.getUserData(UnityLocalAttachProcessDebuggerProvider.PROJECT_NAME_KEY)
- return if (projectName != null) {
- "${process.executableDisplayName} ($projectName)"
- }
- else {
- process.executableDisplayName
- }
+ val displayNames = userData.getUserData(UnityLocalAttachProcessDebuggerProvider.PROCESS_INFO_KEY)?.get(process.pid)
+ val projectName = if (displayNames?.projectName != null) " (${displayNames.projectName})" else ""
+ val roleName = if (displayNames?.roleName != null) " ${displayNames.roleName}" else ""
+ return process.executableDisplayName + roleName + projectName
}
override fun compare(p1: ProcessInfo, p2: ProcessInfo) = p1.pid.compareTo(p2.pid)
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorProfileState.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorProfileState.kt
index 512f70175..495f07888 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorProfileState.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorProfileState.kt
@@ -11,11 +11,11 @@ import com.intellij.util.ui.UIUtil
import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rd.util.reactive.AddRemove
import com.jetbrains.rd.util.reactive.hasTrueValue
-import com.jetbrains.rider.UnityProjectDiscoverer
import com.jetbrains.rider.debugger.DebuggerHelperHost
import com.jetbrains.rider.debugger.DebuggerInitializingState
import com.jetbrains.rider.debugger.DebuggerWorkerProcessHandler
import com.jetbrains.rider.debugger.RiderDebugActiveDotNetSessionsTracker
+import com.jetbrains.rider.isUnityProject
import com.jetbrains.rider.model.rdUnityModel
import com.jetbrains.rider.plugins.unity.UnityHost
import com.jetbrains.rider.plugins.unity.run.UnityDebuggerOutputListener
@@ -68,10 +68,12 @@ class UnityAttachToEditorProfileState(private val remoteConfiguration: UnityAtta
try {
if (!remoteConfiguration.updatePidAndPort()) {
logger.trace("Do not found Unity, starting new Unity Editor")
+
val model = UnityHost.getInstance(project).model
if (UnityInstallationFinder.getInstance(project).getApplicationPath() == null ||
- model.hasUnityReference.hasTrueValue && !UnityProjectDiscoverer.getInstance(project).isUnityProjectFolder)
+ model.hasUnityReference.hasTrueValue && !project.isUnityProject()) {
throw RuntimeConfigurationError("Cannot automatically determine Unity Editor instance. Please open the project in Unity and try again.")
+ }
val args = getUnityWithProjectArgs(project)
if (remoteConfiguration.play) {
@@ -86,7 +88,8 @@ class UnityAttachToEditorProfileState(private val remoteConfiguration: UnityAtta
Thread.sleep(2000)
}
UIUtil.invokeLaterIfNeeded {
- logger.trace("Connecting to Unity Editor with port: $port")
+ logger.trace("DebuggerWorker port: $port")
+ logger.trace("Connecting to Unity Editor with port: ${remoteConfiguration.port}")
super.createWorkerRunCmd(lifetime, helper, port).onSuccess { result.setResult(it) }.onError { result.setError(it) }
}
}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorRunConfiguration.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorRunConfiguration.kt
index eee42e4e6..1e78eb11b 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorRunConfiguration.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorRunConfiguration.kt
@@ -10,6 +10,8 @@ import com.intellij.execution.runners.RunConfigurationWithSuppressedDefaultRunAc
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
+import com.intellij.util.xmlb.annotations.Transient
+import com.jetbrains.rider.*
import com.jetbrains.rider.plugins.unity.run.UnityRunUtil
import com.jetbrains.rider.plugins.unity.util.*
import com.jetbrains.rider.run.configurations.remote.DotNetRemoteConfiguration
@@ -29,9 +31,9 @@ class UnityAttachToEditorRunConfiguration(project: Project, factory: Configurati
}
// Note that we don't serialise these - they will change between sessions, possibly during a session
- override var port: Int = -1
- override var address: String = "127.0.0.1"
- var pid: Int? = null
+ @Transient override var port: Int = -1
+ @Transient override var address: String = "127.0.0.1"
+ @Transient var pid: Int? = null
override fun clone(): RunConfiguration {
val configuration = super.clone() as UnityAttachToEditorRunConfiguration
@@ -68,19 +70,61 @@ class UnityAttachToEditorRunConfiguration(project: Project, factory: Configurati
override var listenPortForConnections: Boolean = false
+ override fun checkSettingsBeforeRun() {
+ // This method lets us check settings before run. If we throw an instance of RuntimeConfigurationError, the Run
+ // Configuration editor is displayed. It's called on the EDT, so there's not a lot we can do - e.g. we can't get
+ // a process list.
+
+ // If we already have a pid, that means this run configuration has been launched before, and we've successfully
+ // attached to a process. Use it again. If the pid is out of date (highly unlikely), we'll do our best to find
+ // the process again
+ if (pid != null) {
+ return
+ }
+
+ // If we're a class library project that isn't in a Unity project folder, we can't guess at the correct project
+ // to attach to, so throw an error and show the dialog. This value will be null until the backend has finished
+ // loading. However, because we're a Unity run configuration, we can safely assume we're a Unity project, and if
+ // we're not inside a Unity project folder, then we can't automatically attach, so throw an error and show the
+ // dialog
+ val isClassLibraryProject = project.isUnityClassLibraryProject()
+ if (!project.isUnityProjectFolder() && (isClassLibraryProject == null || isClassLibraryProject)) {
+ throw RuntimeConfigurationError("Unable to automatically discover correct Unity Editor to debug")
+ }
+ }
+
fun updatePidAndPort() : Boolean {
val processList = OSProcessUtil.getProcessList()
- // Try to reuse the previous process ID, if it's still valid, then fall back to finding the process
- // automatically. Theoretically, there is a tiny chance the previous process has died, and the process ID has
- // been recycled for a new process that just happens to be a Unity process. Practically, this is not likely
- pid = checkValidEditorInstance(pid, processList) ?: findUnityEditorInstanceFromEditorInstanceJson(processList) ?: return false
- port = convertPidToDebuggerPort(pid!!)
- return true
+ port = -1
+
+ try {
+ // Try to reuse the previously attached process ID, if it's still valid. If we don't have a previous pid, or
+ // the process is no longer valid, try to find the best match, via EditorInstance.json or project name.
+ pid = checkValidEditorProcess(pid, processList)
+ ?: findUnityEditorProcessFromEditorInstanceJson(processList)
+ ?: findUnityEditorProcessFromProjectName(processList)
+ if (pid == null) {
+ return false
+ }
+ port = convertPidToDebuggerPort(pid!!)
+ return true
+ }
+ catch(t: Throwable) {
+ pid = null
+ throw t
+ }
+ }
+
+ private fun checkValidEditorProcess(pid: Int?, processList: Array): Int? {
+ if (pid != null && UnityRunUtil.isValidUnityEditorProcess(pid, processList)) {
+ return pid
+ }
+ return null
}
- private fun findUnityEditorInstanceFromEditorInstanceJson(processList: Array): Int? {
+ private fun findUnityEditorProcessFromEditorInstanceJson(processList: Array): Int? {
val editorInstanceJson = EditorInstanceJson.getInstance(project)
if (editorInstanceJson.validateStatus(processList) == EditorInstanceJsonStatus.Valid) {
return editorInstanceJson.contents!!.process_id
@@ -89,23 +133,53 @@ class UnityAttachToEditorRunConfiguration(project: Project, factory: Configurati
return null
}
- private fun checkValidEditorInstance(pid: Int?, processList: Array): Int? {
- if (pid != null && UnityRunUtil.isValidUnityEditorProcess(pid, processList)) {
- return pid
+ private fun findUnityEditorProcessFromProjectName(processList: Array): Int? {
+ // This only works if we can figure out the project name for a running process. This might not succeed on
+ // Windows, if the process is started without appropriate command line args.
+ val unityProcesses = processList.filter { UnityRunUtil.isUnityEditorProcess(it) }
+ val map = UnityRunUtil.getAllUnityProcessInfo(unityProcesses, project)
+
+ // If we're a generated project, or a class library project that lives in the root of a Unity project alongside
+ // a generated project, we can use the project dir as the expected project name.
+ if (project.isUnityProject()) {
+ val expectedProjectName = project.projectDir.name
+ val entry = map.entries.firstOrNull { expectedProjectName.equals(it.value.projectName, true) }
+ if (entry != null) {
+ return entry.key
+ }
+
+ // We don't have a cached pid from a previous debug session, we don't have EditorInstance.json, we can't
+ // find a process with a matching project name. Best guess fallback is to attach to an unnamed project
+ val noNameProjects = map.entries.filter { it.value.projectName == null }
+ if (noNameProjects.count() == 1) {
+ return noNameProjects[0].key
+ }
+
+ return null
+ }
+ else {
+ // We're a class library project in a standalone directory. We can't guess the project name, and it's best
+ // not to attach to a random editor
+ throw RuntimeConfigurationError("Unable to automatically discover correct Unity Editor to debug")
}
- return null
}
- override fun checkConfiguration() {
- // Too expensive to check here?
+ override fun readExternal(element: Element) {
+ super.readExternal(element)
+ // Reset pid, address + port to defaults. It makes no sense to persist the pid across sessions. Unfortunately,
+ // the base class has been serialising them for years...
+ pid = null
+ port = -1
+ address = "127.0.0.1"
}
override fun writeExternal(element: Element) {
super.writeExternal(element)
- // Write it, but don't read it. We need to write it so that the modified check
- // works, but we're not interested in reading it as we will recalculate it
+ // Write it, but don't read it. We need to write it so that the modified check works, but we're not interested
+ // in reading it as we will recalculate it.
+ // TODO: Explain the comment above - what modified check?
if (pid != null) {
- element.setAttribute("pid", pid.toString())
+ element.setAttribute("ignored-value-for-modified-check", pid.toString())
}
}
}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorViewModel.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorViewModel.kt
index d4ba4ba41..e4d263148 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorViewModel.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorViewModel.kt
@@ -2,6 +2,7 @@ package com.jetbrains.rider.plugins.unity.run.configurations
import com.intellij.execution.process.OSProcessUtil
import com.intellij.execution.process.ProcessInfo
+import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.Project
import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rd.util.reactive.IProperty
@@ -10,6 +11,7 @@ import com.jetbrains.rd.util.reactive.ViewableList
import com.jetbrains.rider.plugins.unity.run.UnityRunUtil
import com.jetbrains.rider.plugins.unity.util.EditorInstanceJson
import com.jetbrains.rider.plugins.unity.util.EditorInstanceJsonStatus
+import com.jetbrains.rider.projectDir
import com.jetbrains.rider.util.idea.application
class UnityAttachToEditorViewModel(val lifetime: Lifetime, private val project: Project) {
@@ -27,29 +29,31 @@ class UnityAttachToEditorViewModel(val lifetime: Lifetime, private val project:
fun refreshProcessList() {
editorProcesses.clear()
- val currentModalityState = application.currentModalityState
application.executeOnPooledThread {
val processList = OSProcessUtil.getProcessList()
val editors = getEditorProcessInfos(processList)
- application.invokeLater({ editors.forEach { editorProcesses.add(it) } }, currentModalityState)
-
- editorInstanceJsonStatus.set(editorInstanceJson.validateStatus(processList))
-
- this.pid.value = if (editorInstanceJsonStatus.value != EditorInstanceJsonStatus.Valid && editorProcesses.count() == 1) {
- editorProcesses[0].pid
- } else {
- editorInstanceJson.contents?.process_id
- }
+ application.invokeLater({
+ editorProcesses.addAll(editors)
+ editorInstanceJsonStatus.set(editorInstanceJson.validateStatus(processList))
+ pid.value = if (editorInstanceJsonStatus.value != EditorInstanceJsonStatus.Valid && editors.count() == 1) {
+ editors[0].pid
+ } else if (editorInstanceJson.status == EditorInstanceJsonStatus.Valid) {
+ editorInstanceJson.contents?.process_id
+ } else {
+ // If we're a class library project in the same folder as a Unity project, we can still guess the name
+ editors.firstOrNull { project.projectDir.name.equals(it.projectName, true) }?.pid
+ }
+ }, ModalityState.any())
}
}
private fun getEditorProcessInfos(processList: Array): List {
val unityProcesses = processList.filter { UnityRunUtil.isUnityEditorProcess(it) }
- val projectNames = UnityRunUtil.getUnityProcessProjectNames(unityProcesses, project)
+ val unityProcessInfoMap = UnityRunUtil.getAllUnityProcessInfo(unityProcesses, project)
return unityProcesses.map {
- EditorProcessInfo(it.executableName, it.pid, projectNames[it.pid])
+ EditorProcessInfo(it.executableName, it.pid, unityProcessInfoMap[it.pid]?.projectName)
}
}
}
\ No newline at end of file
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/util/EditorInstanceJson.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/util/EditorInstanceJson.kt
index f3c4ddb3c..c51e8a718 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/util/EditorInstanceJson.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/util/EditorInstanceJson.kt
@@ -49,8 +49,10 @@ data class EditorInstanceJson(val status: EditorInstanceJsonStatus, val contents
}
return try {
- val contents = Gson().fromJson(FileReader(file), EditorInstanceJsonContents::class.java)
- EditorInstanceJson(EditorInstanceJsonStatus.Valid, contents)
+ FileReader(file).use {
+ val contents = Gson().fromJson(it, EditorInstanceJsonContents::class.java)
+ EditorInstanceJson(EditorInstanceJsonStatus.Valid, contents)
+ }
} catch (e: IOException) {
logger.error("Error reading EditorInstance.json", e)
empty(EditorInstanceJsonStatus.Error)
diff --git a/rider/src/main/resources/META-INF/plugin.xml b/rider/src/main/resources/META-INF/plugin.xml
index 81fa518f2..8b26015aa 100644
--- a/rider/src/main/resources/META-INF/plugin.xml
+++ b/rider/src/main/resources/META-INF/plugin.xml
@@ -270,13 +270,24 @@
Changed:
- Entire plugin is no longer disabled if the CSS plugin is disabled (RIDER-36523, #1443)
+ - Rider: Make Attach to Unity Process dialog resizable (#1446, #1450)
+ - Rider: Identify child processes by role in Attach to Unity Process dialog (#1328, #1450)
+
+Fixed:
+
+ - Rider: Show correct project name when Unity started with certain command line on Windows (#1450)
+ - Rider: Show correct project name when multiple Unity processes listed in Attach to Process popup list (#1456, #1450)
+ - Rider: Fix exception in Attach to Unity Process dialog causing list to be empty (#1454, #1450)
+ - Rider: Show run configuration dialog for Unity class library projects (#1445, #1450)
+ - Rider: Fix finding existing Unity instance to debug (RIDER-36256, #1450)
+ - Rider: Fix EditorInstance.json being locked by Rider (#1450)
Fixed:
- Fix usage count for custom event based event handlers in Unity 2018.4+ (#1448, #1449)
-See the CHANGELOG for more details and history.
+See the CHANGELOG for more details and history.
]]>