From ff3cf02869dc5a5ab7f84c3db149a2951421801b Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Tue, 10 Dec 2019 23:56:15 +0000 Subject: [PATCH 01/14] Handle project path containing backslashes --- .../com/jetbrains/rider/plugins/unity/run/UnityRunUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..50b5ac2a8 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 @@ -114,7 +114,7 @@ object UnityRunUtil { while (tokenizer.hasMoreTokens()) { val token = tokenizer.nextToken() if (token.equals("-projectPath", true) || token.equals("-createPath", true)) { - return getProjectNameFromPath(StringUtil.unescapeStringCharacters(tokenizer.nextToken())) + return getProjectNameFromPath(StringUtil.unquoteString(tokenizer.nextToken())) } } } From e6c3882436e0e4097ae7c4c06d0ee8fc9c635de9 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Wed, 11 Dec 2019 17:15:16 +0000 Subject: [PATCH 02/14] Make Attach to Unity Process dialog resizable Fixes #1446 --- .../rider/plugins/unity/run/UnityProcessPickerDialog.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..f9e9111df 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(600, 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 From 88fd4a4bef6c2ae702beab523c6f7657307e7e05 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Thu, 12 Dec 2019 11:39:16 +0000 Subject: [PATCH 03/14] Show Unity child process role in debug list Also fixes issue showing the correct project name in the Attach to Process popup list. Fixes #1328, #1456 --- .../rider/plugins/unity/run/UnityPlayer.kt | 7 +- .../plugins/unity/run/UnityPlayerListener.kt | 7 +- .../unity/run/UnityProcessPickerDialog.kt | 5 +- .../rider/plugins/unity/run/UnityRunUtil.kt | 153 +++++++++++------- ...UnityLocalAttachProcessDebuggerProvider.kt | 12 +- ...nityLocalAttachProcessPresentationGroup.kt | 11 +- .../UnityAttachToEditorViewModel.kt | 4 +- 7 files changed, 124 insertions(+), 75 deletions(-) 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 f9e9111df..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,7 +62,7 @@ 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 { preferredSize = Dimension(600, 450) } + }.apply { preferredSize = Dimension(650, 450) } isOKActionEnabled = false cancelAction.putValue(FOCUSED_ACTION, true) @@ -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 50b5ac2a8..5aa243000 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,19 @@ 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 } + val projectName = getProjectNameFromEditorInstanceJson(it, project) + parseProcessInfoFromCommandLine(it, projectName)?.let { n -> processInfoMap[it.pid] = n } } - 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 +109,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.unquoteString(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 + } - i++ - } while (i < commandLineArgs.size) + return UnityProcessInfo(projectName, name ?: umpWindowTitle ?: umpProcessRole) + } - return null + private fun tokenizeCommandLine(processInfo: ProcessInfo): List { + return tokenizeQuotedCommandLine(processInfo) + ?: tokenizeUnquotedCommandLine(processInfo) + } + + 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 + } + } + + 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 +234,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 +255,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 +288,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/UnityAttachToEditorViewModel.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorViewModel.kt index d4ba4ba41..aa5c4fafb 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 @@ -47,9 +47,9 @@ class UnityAttachToEditorViewModel(val lifetime: Lifetime, private val project: 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 From 316f4dbac57783b88f0710988783aca5b27d1e31 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Thu, 12 Dec 2019 11:57:32 +0000 Subject: [PATCH 04/14] Stop any exception killing the Unity process list Fixes #1454 --- .../jetbrains/rider/plugins/unity/run/UnityRunUtil.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 5aa243000..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 @@ -81,8 +81,13 @@ object UnityRunUtil { val processInfoMap = mutableMapOf() processList.forEach { - val projectName = getProjectNameFromEditorInstanceJson(it, project) - parseProcessInfoFromCommandLine(it, projectName)?.let { n -> processInfoMap[it.pid] = n } + 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 we failed to get project name from the command line, try and get it from the working directory From dc4b4f0b97be302cc770ddc46bb4e8ac7f067013 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Sun, 15 Dec 2019 15:46:47 +0000 Subject: [PATCH 05/14] Show dialog if we don't know which process to debug Fixes #1445 --- .../UnityAttachToEditorRunConfiguration.kt | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) 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..478bd1229 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,7 @@ 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.plugins.unity.run.UnityRunUtil import com.jetbrains.rider.plugins.unity.util.* import com.jetbrains.rider.run.configurations.remote.DotNetRemoteConfiguration @@ -29,9 +30,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,6 +69,25 @@ 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 theres' 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 (pid != null) { + return + } + + // Verify that we have an EditorInstance.json. If we don't, we can't easily tell that any running instances are + // for our project. + val editorInstanceJson = EditorInstanceJson.getInstance(project) + if (editorInstanceJson.status != EditorInstanceJsonStatus.Valid) { + throw RuntimeConfigurationError("Unable to automatically discover correct Unity Editor to debug") + } + } + fun updatePidAndPort() : Boolean { val processList = OSProcessUtil.getProcessList() @@ -100,12 +120,22 @@ class UnityAttachToEditorRunConfiguration(project: Project, factory: Configurati // 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()) } } } From efa00cd9357304dffdafffda3d594be152ffb9df Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Sun, 15 Dec 2019 15:47:11 +0000 Subject: [PATCH 06/14] Refresh dialog correctly on initial load --- .../UnityAttachToEditorViewModel.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 aa5c4fafb..6b1e814f4 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 @@ -27,21 +28,20 @@ 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 { + editorInstanceJson.contents?.process_id + } + }, ModalityState.any()) } } From 7a18e9a28d20c7d85e0127ecda10cc28a4616f5e Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Mon, 16 Dec 2019 08:54:56 +0000 Subject: [PATCH 07/14] Reset pid if we can't find a valid Unity process --- .../configurations/UnityAttachToEditorRunConfiguration.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 478bd1229..3833e122f 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 @@ -95,7 +95,11 @@ class UnityAttachToEditorRunConfiguration(project: Project, factory: Configurati // 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 = -1 + pid = checkValidEditorInstance(pid, processList) ?: findUnityEditorInstanceFromEditorInstanceJson(processList) + if (pid == null) { + return false + } port = convertPidToDebuggerPort(pid!!) return true } From 88488fd5c1c3dd9c67ed67e2c79bcb8e4055a499 Mon Sep 17 00:00:00 2001 From: Ivan Shakhov Date: Mon, 16 Dec 2019 10:42:49 +0100 Subject: [PATCH 08/14] correct logging --- .../run/configurations/UnityAttachToEditorProfileState.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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..78db1bc26 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 @@ -86,7 +86,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) } } } From dfe387022ff4173f38453f3fa68b39d8b5118223 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Mon, 16 Dec 2019 10:22:04 +0000 Subject: [PATCH 09/14] Improve finding Unity editor to debug If the previously cached pid is no longer valid, will now check project name as well as EditorInstance.json --- .../jetbrains/rider/UnityProjectDiscoverer.kt | 2 +- .../UnityAttachToEditorProfileState.kt | 6 +- .../UnityAttachToEditorRunConfiguration.kt | 81 ++++++++++++++----- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt b/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt index 6bad0c45a..8b4b5da61 100644 --- a/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt +++ b/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt @@ -19,7 +19,7 @@ class UnityProjectDiscoverer(project: Project, unityHost: UnityHost) : Lifetimed // anywhere) val isUnityProject = isUnityProjectFolder && isCorrectlyLoadedSolution(project) val isUnityGeneratedProject = isUnityProject && solutionNameMatchesUnityProjectName(project) - val isUnitySidecarProject = isUnityProject && !solutionNameMatchesUnityProjectName(project) + val isUnityClassLibraryProject = isUnityProject && !solutionNameMatchesUnityProjectName(project) companion object { fun getInstance(project: Project) = project.getComponent() 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..c4dd96d97 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) { 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 3833e122f..2ff7a6446 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 @@ -11,6 +11,7 @@ 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 @@ -71,17 +72,22 @@ class UnityAttachToEditorRunConfiguration(project: Project, factory: Configurati 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 theres' not a lot we can do - e.g. we can't get + // 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. + // 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 } // Verify that we have an EditorInstance.json. If we don't, we can't easily tell that any running instances are // for our project. + // This means: + // * If Unity isn't running, we show the dialog. We never try to launch Unity ourselves + // * If EditorInstance.json doesn't exist (for some reason), we show the dialog + // * All other scenarios, we have EditorInstance.json and know exactly which instance to attach to val editorInstanceJson = EditorInstanceJson.getInstance(project) if (editorInstanceJson.status != EditorInstanceJsonStatus.Valid) { throw RuntimeConfigurationError("Unable to automatically discover correct Unity Editor to debug") @@ -92,19 +98,36 @@ class UnityAttachToEditorRunConfiguration(project: Project, factory: Configurati 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 port = -1 - pid = checkValidEditorInstance(pid, processList) ?: findUnityEditorInstanceFromEditorInstanceJson(processList) - if (pid == null) { - return false + + 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. + // The only way we'll match on project name is if a previously cached pid turns out to be invalid, and the + // new process hasn't created an EditorInstance.json for some reason. + 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 } - port = convertPidToDebuggerPort(pid!!) - return true } - private fun findUnityEditorInstanceFromEditorInstanceJson(processList: Array): Int? { + private fun checkValidEditorProcess(pid: Int?, processList: Array): Int? { + if (pid != null && UnityRunUtil.isValidUnityEditorProcess(pid, processList)) { + return pid + } + return null + } + + private fun findUnityEditorProcessFromEditorInstanceJson(processList: Array): Int? { val editorInstanceJson = EditorInstanceJson.getInstance(project) if (editorInstanceJson.validateStatus(processList) == EditorInstanceJsonStatus.Valid) { return editorInstanceJson.contents!!.process_id @@ -113,15 +136,35 @@ 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 - } - return null - } + 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 + } - override fun checkConfiguration() { - // Too expensive to check here? + // 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") + } } override fun readExternal(element: Element) { From f6e24c1586d1a00565b50a6bc433396d110a09fa Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Mon, 16 Dec 2019 10:26:18 +0000 Subject: [PATCH 10/14] Select Unity instance based on project name --- .../run/configurations/UnityAttachToEditorViewModel.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 6b1e814f4..a1dbd0664 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 @@ -8,9 +8,11 @@ import com.jetbrains.rd.util.lifetime.Lifetime import com.jetbrains.rd.util.reactive.IProperty import com.jetbrains.rd.util.reactive.Property import com.jetbrains.rd.util.reactive.ViewableList +import com.jetbrains.rider.isUnityProject 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) { @@ -38,8 +40,11 @@ class UnityAttachToEditorViewModel(val lifetime: Lifetime, private val project: editorInstanceJsonStatus.set(editorInstanceJson.validateStatus(processList)) pid.value = if (editorInstanceJsonStatus.value != EditorInstanceJsonStatus.Valid && editors.count() == 1) { editors[0].pid - } else { + } 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()) } From 1ba9bc79123d70559cde87eb2712a657dd08f340 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Mon, 16 Dec 2019 11:04:58 +0000 Subject: [PATCH 11/14] Only show dialog for class library projects --- .../com/jetbrains/rider/UnityProjectDiscoverer.kt | 5 ++++- .../UnityAttachToEditorRunConfiguration.kt | 13 +++---------- .../configurations/UnityAttachToEditorViewModel.kt | 1 - 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt b/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt index 8b4b5da61..7f7ea246f 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 @@ -19,7 +20,8 @@ class UnityProjectDiscoverer(project: Project, unityHost: UnityHost) : Lifetimed // anywhere) val isUnityProject = isUnityProjectFolder && isCorrectlyLoadedSolution(project) val isUnityGeneratedProject = isUnityProject && solutionNameMatchesUnityProjectName(project) - val isUnityClassLibraryProject = isUnityProject && !solutionNameMatchesUnityProjectName(project) + val isUnitySidecarProject = isUnityProject && !solutionNameMatchesUnityProjectName(project) + val isUnityClassLibraryProject = hasUnityReference.valueOrDefault(false) && isCorrectlyLoadedSolution(project) companion object { fun getInstance(project: Project) = project.getComponent() @@ -58,5 +60,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/configurations/UnityAttachToEditorRunConfiguration.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/run/configurations/UnityAttachToEditorRunConfiguration.kt index 2ff7a6446..c13da77ee 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 @@ -82,14 +82,9 @@ class UnityAttachToEditorRunConfiguration(project: Project, factory: Configurati return } - // Verify that we have an EditorInstance.json. If we don't, we can't easily tell that any running instances are - // for our project. - // This means: - // * If Unity isn't running, we show the dialog. We never try to launch Unity ourselves - // * If EditorInstance.json doesn't exist (for some reason), we show the dialog - // * All other scenarios, we have EditorInstance.json and know exactly which instance to attach to - val editorInstanceJson = EditorInstanceJson.getInstance(project) - if (editorInstanceJson.status != EditorInstanceJsonStatus.Valid) { + // 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 + if (project.isUnityClassLibraryProject() && !project.isUnityProjectFolder()) { throw RuntimeConfigurationError("Unable to automatically discover correct Unity Editor to debug") } } @@ -103,8 +98,6 @@ class UnityAttachToEditorRunConfiguration(project: Project, factory: Configurati 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. - // The only way we'll match on project name is if a previously cached pid turns out to be invalid, and the - // new process hasn't created an EditorInstance.json for some reason. pid = checkValidEditorProcess(pid, processList) ?: findUnityEditorProcessFromEditorInstanceJson(processList) ?: findUnityEditorProcessFromProjectName(processList) 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 a1dbd0664..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 @@ -8,7 +8,6 @@ import com.jetbrains.rd.util.lifetime.Lifetime import com.jetbrains.rd.util.reactive.IProperty import com.jetbrains.rd.util.reactive.Property import com.jetbrains.rd.util.reactive.ViewableList -import com.jetbrains.rider.isUnityProject import com.jetbrains.rider.plugins.unity.run.UnityRunUtil import com.jetbrains.rider.plugins.unity.util.EditorInstanceJson import com.jetbrains.rider.plugins.unity.util.EditorInstanceJsonStatus From 429569b89ababcfe6b6c33bae37f500cbd00ddde Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Mon, 16 Dec 2019 11:20:24 +0000 Subject: [PATCH 12/14] Fix showing dialog for class library projects --- .../kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt | 8 +++++++- .../configurations/UnityAttachToEditorRunConfiguration.kt | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt b/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt index 7f7ea246f..fabe86f19 100644 --- a/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt +++ b/rider/src/main/kotlin/com/jetbrains/rider/UnityProjectDiscoverer.kt @@ -21,7 +21,13 @@ class UnityProjectDiscoverer(project: Project, unityHost: UnityHost) : Lifetimed val isUnityProject = isUnityProjectFolder && isCorrectlyLoadedSolution(project) val isUnityGeneratedProject = isUnityProject && solutionNameMatchesUnityProjectName(project) val isUnitySidecarProject = isUnityProject && !solutionNameMatchesUnityProjectName(project) - val isUnityClassLibraryProject = hasUnityReference.valueOrDefault(false) && isCorrectlyLoadedSolution(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() 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 c13da77ee..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 @@ -83,8 +83,12 @@ class UnityAttachToEditorRunConfiguration(project: Project, factory: Configurati } // 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 - if (project.isUnityClassLibraryProject() && !project.isUnityProjectFolder()) { + // 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") } } From 648a5bd95af0d899a6b64af7333a9edd94d095a5 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Mon, 16 Dec 2019 11:30:37 +0000 Subject: [PATCH 13/14] Ensure EditorInstance isn't locked after reading --- .../rider/plugins/unity/util/EditorInstanceJson.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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) From 42001c540d97eecbc0686f49cba597a12ef0c974 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Mon, 16 Dec 2019 11:43:26 +0000 Subject: [PATCH 14/14] Update CHANGELOG and plugin metadata --- CHANGELOG.md | 17 ++++++++++++++--- rider/src/main/resources/META-INF/plugin.xml | 13 ++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e132539..ef7a0ca23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,27 @@ 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 -- Added proper file icons for `*.uxml` and `*.uss` ([RIDER-34788](https://youtrack.jetbrains.com/issue/RIDER-34788), [#1443](https://github.com/JetBrains/resharper-unity/pull/1443)) +- Rider: Added proper file icons for `*.uxml` and `*.uss` ([RIDER-34788](https://youtrack.jetbrains.com/issue/RIDER-34788), [#1443](https://github.com/JetBrains/resharper-unity/pull/1443)) ### Changed -- 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: 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 + +- 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)) diff --git a/rider/src/main/resources/META-INF/plugin.xml b/rider/src/main/resources/META-INF/plugin.xml index 22086b88a..391e8c92c 100644 --- a/rider/src/main/resources/META-INF/plugin.xml +++ b/rider/src/main/resources/META-INF/plugin.xml @@ -270,9 +270,20 @@ 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)

-

See the CHANGELOG for more details and history.

+

See the CHANGELOG for more details and history.

]]>