如何让Visual Studio Tools for Unity插件用于调试你自己的Mono嵌入应用程序

     这篇文章是关于UnrealSharp开发过程中,支持使用Visual Studio调试集成到UnrealEngine5中去的C#代码的总结。

     关于UnrealSharp: https://www.cnblogs.com/bodong/p/18063520

     --------------------

     最近在测试将mono嵌入到C++应用程序中,苦于没有调试器,有时候还是不怎么方便。网上搜了一下,有VS插件MDebug、VSMonoDebugger,实际试用了一下,有点麻烦,而且似乎对Windows+Visual Studio 2022支持不大好。因此想到了,Unity引擎是基于mono的,Visual Studio 2022也内置了针对Unity的调试器,名为:Visual Studio Tools for Unity。我想如果这个插件也能调试我的应用程序就好了。

     打开VS,使用菜单中的“附加到Unity”菜单打开附加对话框。最后发现并不能识别我的mono嵌入应用程序。因此直接调试Visual Studio 2022,查找和研究VS发现Unity进程的方法。经过一系列的调试,发现查找Unity相关进程的代码位于:SyntaxTree.VisualStudio.Unity.Messaging.dll 中,文件路径:

// c:\program files\microsoft visual studio\2022\professional\common7\ide\extensions\microsoft\visual studio tools for unity\SyntaxTree.VisualStudio.Unity.Messaging.dll
// SyntaxTree.VisualStudio.Unity.Messaging, Version=17.8.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a

     具体路径可能你跟我不一样。下面这个函数位于:SyntaxTree.VisualStudio.Unity.Messaging.UnityProbe类中。

public static IEnumerable<UnityProcess> GetUnityProcesses(string informationFormat = null, bool localPlayerProcessDetection = false)
{
    Process[] array = SafeProcess.GetProcesses().ToArray<Process>();
    IEnumerable<UnityProcess> enumerable = from p in array
        where p.ProcessProperty((Process _) => _.ProcessName) == "Unity"
        select UnityProbe.UnityProcessFor(p, UnityProcessType.Editor, UnityProbe.GetDebuggerPort(p.Id), informationFormat);
    if (!localPlayerProcessDetection)
    {
        return enumerable;
    }
    IEnumerable<UnityProcess> enumerable2 = from p in array.Where(new Func<Process, bool>(UnityProbe.IsLocalPlayerProcess))
        select UnityProbe.UnityProcessFor(p, UnityProcessType.Player, 0, informationFormat);
    return enumerable.Concat(enumerable2);
}

      我们可以看到,这里使用了两种方法来探查Unity相关进程。其一是直接查找名字叫Unity的进程,其二是探查可能是Unity Player(即Windows PC档)的进程。前者直接查看进程名称,后者通过下面这个函数来判断:

public static bool IsLocalPlayerProcess(Process process)
{
    bool flag;
    try
    {
        if (process.MainWindowHandle == IntPtr.Zero)
        {
            flag = false;
        }
        else
        {
            ProcessModule mainModule = process.MainModule;
            if (mainModule == null)
            {
                flag = false;
            }
            else
            {
                string fileName = mainModule.FileName;
                string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
                string directoryName = Path.GetDirectoryName(fileName);
                if (directoryName == null)
                {
                    flag = false;
                }
                else
                {
                    flag = Directory.Exists(Path.Combine(directoryName, fileNameWithoutExtension + "_Data")) && File.Exists(Path.Combine(directoryName, "UnityPlayer.dll"));
                }
            }
        }
    }
    catch (Exception)
    {
        flag = false;
    }
    return flag;
}

      可以看到,只要目标进程目录下有一个 “进程名(无扩展名)” + "_Data"的目录,且该目录下有一个UnityPlayer.dll,即可被视为Unity相关进程。

  因此要将我们自己的进程被该插件识别到,也有两种方法,假如我们的项目叫MyApp。那么其一是让我们生成的进程也叫Unity.exe;其二是在MyApp.exe所在目录下,新增一个MyApp_Data,然后再随便新建一个空白文本文件,把名称(含扩展名)改成"UnityPlayer.dll"即可。

       这样执行后,你就会发现,你的进程出现在了搜索对话框中了。

       当然,如果此时你直接双击连接是无法连接成功的,因为前面的任务只是让你可以被找到,如果要被连接上,还有另外一些额外的要求。让我们看一下下面这个函数:

public static int GetDebuggerPort(int processId)
{
    return 56000 + processId % 1000;
}

    可以看到,调试器假定了目标端口和进程id之间的关联关系,因此在我们初始化mono的时候,也需要考虑到这一点,因此,我们初始化mono调试器的时候,应该这样:

int DebuggerPort = 56000 + GetProcessId() % 1000;

std::string argument = std::string("--debugger-agent=transport=dt_socket,embedding=1,server=y,suspend=n,address=127.0.0.1:") + ToStlString(DebuggerPort);

const char* options[] = 
{
   argument.c_str()
};

mono_jit_parse_options(sizeof(options)/sizeof(options[0]), (char**)options);
mono_debug_init(MONO_DEBUG_FORMAT_MONO);

    这样你就可以白嫖Visual Studio Tools for Unity,用于调试你自己的mono嵌入程序了。

 

 

 --------------------------------------------------------- --------------------------------------------------------- --------------------------------------------------------- --------------------------------------------------------- ---------------------------------------------------------

2024.7.17 更新

 

PS : 从VS2022的某一个版本开始,由于VS for Unity插件升级了,导致前面的规则失效了。相关的变更想必与适配新的Unity引擎有关系。对于UnityEditor来说,并没有区别,主要区别在于对UnityPlayer的处理。

下面是识别进程的相关代码,基本逻辑没变化。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;

namespace SyntaxTree.VisualStudio.Unity.Messaging
{
    // Token: 0x02000025 RID: 37
    [NullableContext(1)]
    [Nullable(0)]
    public static class UnityProbe
    {
        // Token: 0x060000B7 RID: 183 RVA: 0x000047DB File Offset: 0x000029DB
        public static IEnumerable<UnityProcess> GetUnityProcesses([Nullable(2)] string informationFormat = null, bool localPlayerProcessDetection = false)
        {
            Process[] array = SafeProcess.GetProcesses().ToArray<Process>();
            foreach (Process process in array)
            {
                DebuggerEngine debuggerEngine;
                if (UnityProbe.IsLocalEditorProcess(process, out debuggerEngine))
                {
                    yield return UnityProbe.UnityProcessFor(process, UnityProcessType.Editor, informationFormat, debuggerEngine);
                }
                else if (localPlayerProcessDetection && UnityProbe.IsLocalPlayerProcess(process, out debuggerEngine))
                {
                    yield return UnityProbe.UnityProcessFor(process, UnityProcessType.Player, informationFormat, debuggerEngine);
                }
            }
            Process[] array2 = null;
            yield break;
        }

        // Token: 0x060000B8 RID: 184 RVA: 0x000047F4 File Offset: 0x000029F4
        public static bool IsLocalEditorProcess(Process process, out DebuggerEngine debuggerEngine)
        {
            debuggerEngine = DebuggerEngine.VSTU;
            bool flag;
            try
            {
                if (!process.ProcessProperty((Process p) => p.ProcessName.Contains("Unity")))
                {
                    flag = false;
                }
                else if (!process.ProcessProperty((Process p) => p.MainWindowHandle != IntPtr.Zero))
                {
                    flag = false;
                }
                else
                {
                    string directoryName = Path.GetDirectoryName(process.ProcessProperty(delegate(Process p)
                    {
                        ProcessModule mainModule = p.MainModule;
                        if (mainModule == null)
                        {
                            return null;
                        }
                        return mainModule.FileName;
                    }));
                    if (directoryName == null)
                    {
                        flag = false;
                    }
                    else
                    {
                        if (Directory.Exists(Path.Combine(directoryName, "CoreCLR")))
                        {
                            debuggerEngine = DebuggerEngine.CoreCLR;
                        }
                        flag = Directory.Exists(Path.Combine(directoryName, "Data"));
                    }
                }
            }
            catch (Exception)
            {
                flag = false;
            }
            return flag;
        }

        // Token: 0x060000B9 RID: 185 RVA: 0x000048D0 File Offset: 0x00002AD0
        public static bool IsLocalPlayerProcess(Process process, out DebuggerEngine debuggerEngine)
        {
            debuggerEngine = DebuggerEngine.VSTU;
            bool flag;
            try
            {
                if (!process.ProcessProperty((Process p) => p.MainWindowHandle != IntPtr.Zero))
                {
                    flag = false;
                }
                else
                {
                    string text = process.ProcessProperty(delegate(Process p)
                    {
                        ProcessModule mainModule = p.MainModule;
                        if (mainModule == null)
                        {
                            return null;
                        }
                        return mainModule.FileName;
                    });
                    string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(text);
                    string directoryName = Path.GetDirectoryName(text);
                    if (directoryName == null)
                    {
                        flag = false;
                    }
                    else if (!Directory.Exists(Path.Combine(directoryName, fileNameWithoutExtension + "_Data")))
                    {
                        flag = false;
                    }
                    else
                    {
                        if (Directory.Exists(Path.Combine(directoryName, "CoreCLR")))
                        {
                            debuggerEngine = DebuggerEngine.CoreCLR;
                        }
                        flag = File.Exists(Path.Combine(directoryName, "UnityPlayer.dll"));
                    }
                }
            }
            catch (Exception)
            {
                flag = false;
            }
            return flag;
        }

        // Token: 0x060000BA RID: 186 RVA: 0x000049A4 File Offset: 0x00002BA4
        [NullableContext(2)]
        [return: Nullable(1)]
        public static string GetInformation(Process process, string informationFormat)
        {
            if (string.IsNullOrEmpty(informationFormat))
            {
                return string.Empty;
            }
            string text = informationFormat.Replace(string.Format("{{{0}}}", InformationField.ProcessId), ((process != null) ? process.Id.ToString() : null) ?? string.Empty);
            string text2 = string.Format("{{{0}}}", InformationField.ProcessWindowTitle);
            string text3;
            if (process == null)
            {
                text3 = null;
            }
            else
            {
                text3 = process.ProcessProperty((Process p) => p.MainWindowTitle);
            }
            return text.Replace(text2, text3 ?? string.Empty);
        }

        // Token: 0x060000BB RID: 187 RVA: 0x00004A3C File Offset: 0x00002C3C
        public static UnityProcess UnityProcessFor(Process process, UnityProcessType type, [Nullable(2)] string informationFormat = null, DebuggerEngine debuggerEngine = DebuggerEngine.VSTU)
        {
            if (process == null)
            {
                throw new ArgumentNullException("process");
            }
            string text = string.Empty;
            if (Platform.OnWindows())
            {
                string text2 = process.ProcessProperty((Process p) => p.MainWindowTitle);
                text = ((type == UnityProcessType.Editor) ? UnityProbe.ExtractProject(text2) : text2);
            }
            UnityProcess unityProcess = new UnityProcess
            {
                Address = "127.0.0.1",
                Machine = Dns.GetHostName(),
                ProcessId = process.Id,
                ProjectName = text,
                ProfilerPort = 0,
                Type = type,
                DebuggerEngine = debuggerEngine,
                IsProcessDetection = true,
                Information = UnityProbe.GetInformation(process, informationFormat)
            };
            if (type == UnityProcessType.Editor)
            {
                ushort debuggerPort = UnityPorts.GetDebuggerPort((long)process.Id);
                unityProcess.DebuggerPort = (int)debuggerPort;
                unityProcess.MessagerPort = (int)((debuggerPort > 0) ? (debuggerPort + 2) : 0);
            }
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                unityProcess.IsBackground = process.ProcessProperty((Process p) => p.MainWindowHandle == IntPtr.Zero);
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                unityProcess.IsBackground = DarwinProcess.IsBackgroundProcess(process.Id);
            }
            return unityProcess;
        }

        // Token: 0x060000BC RID: 188 RVA: 0x00004B70 File Offset: 0x00002D70
        public static string ExtractProject(string windowTitle)
        {
            string text = "project";
            if (string.IsNullOrEmpty(windowTitle))
            {
                return string.Empty;
            }
            Match match = UnityProbe._legacyProjectRegex.Match(windowTitle);
            if (!match.Success)
            {
                match = UnityProbe._projectRegex.Match(windowTitle);
                if (!Platform.OnWindows())
                {
                    text = "scene";
                }
            }
            if (!match.Success)
            {
                return string.Empty;
            }
            return UnityProbe.ExtractProjectName(match.Groups[text].Value.Trim());
        }

        // Token: 0x060000BD RID: 189 RVA: 0x00004BE8 File Offset: 0x00002DE8
        private static string ExtractProjectName(string unityProject)
        {
            if (unityProject.EndsWith("]", StringComparison.OrdinalIgnoreCase))
            {
                int num = unityProject.LastIndexOf('[');
                if (num > 0)
                {
                    return unityProject.Substring(0, num).Trim();
                }
            }
            return unityProject;
        }

        // Token: 0x04000076 RID: 118
        private static readonly Regex _legacyProjectRegex = new Regex("Unity( [^-]*)?( - \\[.*\\])? - (?<scene>.*(\\.unity)|(Untitled)) - (?<project>.*) - (?<platform>[^-]*)", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);

        // Token: 0x04000077 RID: 119
        private static readonly Regex _projectRegex = new Regex("(?<project>.*) - (?<scene>.*) - (?<platform>[^-]*) - Unity( [^-]*)?( - \\[.*\\])?", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
    }
}

     下面是进一步加工进程数据的代码:

 

public static UnityProcess UnityProcessFor(Process process, UnityProcessType type, [Nullable(2)] string informationFormat = null, DebuggerEngine debuggerEngine = DebuggerEngine.VSTU)
{
    if (process == null)
    {
        throw new ArgumentNullException("process");
    }
    string text = string.Empty;
    if (Platform.OnWindows())
    {
        string text2 = process.ProcessProperty((Process p) => p.MainWindowTitle);
        text = ((type == UnityProcessType.Editor) ? UnityProbe.ExtractProject(text2) : text2);
    }
    UnityProcess unityProcess = new UnityProcess
    {
        Address = "127.0.0.1",
        Machine = Dns.GetHostName(),
        ProcessId = process.Id,
        ProjectName = text,
        ProfilerPort = 0,
        Type = type,
        DebuggerEngine = debuggerEngine,
        IsProcessDetection = true,
        Information = UnityProbe.GetInformation(process, informationFormat)
    };
    if (type == UnityProcessType.Editor)
    {
        ushort debuggerPort = UnityPorts.GetDebuggerPort((long)process.Id);
        unityProcess.DebuggerPort = (int)debuggerPort;
        unityProcess.MessagerPort = (int)((debuggerPort > 0) ? (debuggerPort + 2) : 0);
    }
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    {
        unityProcess.IsBackground = process.ProcessProperty((Process p) => p.MainWindowHandle == IntPtr.Zero);
    }
    else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
    {
        unityProcess.IsBackground = DarwinProcess.IsBackgroundProcess(process.Id);
    }
    return unityProcess;
}

      可以看到,只有当进程是编辑器时才会分配对应的DebugerPort,这估计是为了兼容旧版本的Unity引擎。这样的特殊处理会由于下面的代码存在导致伪装的UnityPlayer不能被识别到:

private void AddProcess(UnityProcess unityProcess)
{
    if (UnityPlayerEnhancer.RequiresEnhancement(unityProcess))
    {
        if (!UnityPlayerEnhancer.TryEnhanceLocalPlayer(unityProcess, UnityInstanceSelectionDialog.RefreshTCPListeners()))
        {
            return;
        }
        UnityInstanceSelectionDialog.UpdateInformation(unityProcess);
    }
    UnityProcess unityProcess2;
    bool flag = this._processCache.TryGetValue(unityProcess, out unityProcess2);
    bool flag2 = flag && UnityPlayerEnhancer.RequiresEnhancement(unityProcess2);
    bool flag3 = flag && UnityInstanceSelectionDialog.IsExtraInformationPresent(unityProcess, unityProcess2);
    if (flag)
    {
        if (!flag2 && !flag3)
        {
            return;
        }
        this._processCache.Remove(unityProcess2);
        this._processes.Remove(unityProcess2);
    }
    this._processCache.Add(unityProcess);
    this._processes.Add(unityProcess);
    this.ProcessFilter.Items.Clear();
    foreach (UnityProcess unityProcess3 in this._processes)
    {
        this.ProcessFilter.Items.Add(unityProcess3);
    }
    this.InstanceList.UpdateColumnWidths();
}

public static bool RequiresEnhancement(UnityProcess unityProcess)
{
    return UnityPlayerEnhancer.IsLocalPlayer(unityProcess) && ((unityProcess != null && unityProcess.IsProcessDetection && unityProcess.DebuggerEngine == DebuggerEngine.VSTU && unityProcess.DebuggerPort == 0) || (unityProcess != null && !unityProcess.IsProcessDetection && unityProcess.ProcessId == 0));
}

private static bool TryEnhanceLocalDetectionPlayer(UnityProcess unityProcess, TcpListenerInfo[] listeners)
{
    int num = UnityPlayerEnhancer.FindFirstPortInRange(unityProcess, listeners, 56000, 56999);
    int num2 = UnityPlayerEnhancer.FindFirstPortInRange(unityProcess, listeners, 55000, 55999);
    if (num2 == 0)
    {
        return false;  // 如果没有Profiler监听端口,那么将无法被添加到列表中。
    }
    unityProcess.DebuggerPort = num;
    unityProcess.ProfilerPort = num2;
    return true;
}

// Token: 0x0600009D RID: 157 RVA: 0x00003D98 File Offset: 0x00001F98
private static int FindFirstPortInRange(UnityProcess unityProcess, TcpListenerInfo[] listeners, int minPort, int maxPort)
{
    return (from e in listeners
        where e.ProcessId == unityProcess.ProcessId && e.LocalPort >= minPort && e.LocalPort <= maxPort
        select e.LocalPort).FirstOrDefault<int>();
}

     从这些代码我们可以看出,现在对于UnityPlayer来说,需要必须存在两个tcpip监听端口,一个是调试端口,范围56000到56999;另一个是Profiler端口,范围55000到55999。

using System;

namespace SyntaxTree.VisualStudio.Unity.Messaging
{
    // Token: 0x02000023 RID: 35
    public class UnityPorts
    {
        // Token: 0x060000B5 RID: 181 RVA: 0x000047C1 File Offset: 0x000029C1
        public static ushort GetDebuggerPort(long processIdOrGuid)
        {
            return (ushort)(56000L + processIdOrGuid % 1000L);
        }

        // Token: 0x0400006F RID: 111
        public const int MinDebuggerPort = 56000;

        // Token: 0x04000070 RID: 112
        public const int MaxDebuggerPort = 56999;

        // Token: 0x04000071 RID: 113
        public const int MinProfilerPort = 55000;

        // Token: 0x04000072 RID: 114
        public const int MaxProfilerPort = 55999;
    }
}

     现在问题明确了,解决方案也就明确了。一种办法是通过mono_profiler_load这个api初始化profiler。但是mono将这个代码提取到了一个独立的dll中,mono-profiler-log.dll等。在我的应用场景中,需要考虑支持dll可重入,而新的mono并不支持可重入。所以这个方案对我来说无效。当然如果你的应用场景中不考虑可重入,那么可以用这个方案。

     我采用的另外一个方法,主动监听一个符合要求的端口,假装这个端口就是Unity调试插件需要的Profiler监听端口,在UnrealEngine5中的代码如下:

#include "MonoProfilerService.h"

#if WITH_MONO
#include "Networking.h"
#include "Sockets.h"
#include "SocketSubsystem.h"

namespace UnrealSharp::Mono
{
    FMonoProfilerService::FMonoProfilerService(int InPort) :
        DefaultPort(InPort)
    {
        FIPv4Address IPAddress;
        FIPv4Address::Parse(TEXT("127.0.0.1"), IPAddress);

        const FIPv4Endpoint Endpoint(IPAddress, DefaultPort);

        ListenerSocket = FTcpSocketBuilder(TEXT("TcpListener"))
           .AsReusable()
           .BoundToEndpoint(Endpoint)
           .Listening(8);
    }

    FMonoProfilerService::~FMonoProfilerService()
    {
        if(ListenerSocket)
        {
            ListenerSocket->Close();

            ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ListenerSocket);
            ListenerSocket = nullptr;
        }
    }
}

       有了这个支持之后,你伪装的UnityPlayer进程又回来了:

   

 

posted @ 2024-01-13 16:47  bodong  阅读(86)  评论(0编辑  收藏  举报