如何使用EnvDTE作为VS2015项目/解决方案的一部分,以便在不使用VS扩展的情况下更改自己的设置?

时间:2016-09-20 16:01:17

标签: visual-studio powershell visual-studio-2015 nuget

更新:我已经开发了自己的PowerShell解决方案(在答案中),但我仍然想知道是否有人知道模仿其行为的NuGet包废弃的StudioShell项目。

我尝试使用EnvDTE在我的Visual Studio 2015解决方案/项目中仅使用pre / post-build脚本,NuGet包,使一些有问题的设置持久(无论未来的开发者做什么)或者VS的一些内置功能。 (我将这些设置/配置更改称为"调整"从此处开始

最终目标是:

  1. 无需外部工具/扩展程序 - 无论我使用什么来进行这些更改都应该是Visual Studio 2015的标准部分和/或应该随解决方案移植,或者它应该是合理地预计它将成为最新Windows操作系统(Windows 7 +)的一部分。

    换句话说,继承我的工作的未来开发人员应该能够从源代码控制中提取解决方案并构建它,而无需安装VS扩展" X",进行更改" Y&#34 ;在他们的构建环境中,或改变" Z"在用户特定的项目设置中。

  2. 可发现 - 用于进行调整的组件/工具/方法应该在Visual Studio界面的某处公开,而不需要比解决方案树更深入(包括上下文菜单,项目属性,等) - 因此可以在不进行太多挖掘的情况下更改或禁用自动调整。

    我不介意将GUI元素作为某些代码的链接。我希望未来的开发人员可以找出代码,但是他们需要能够轻松发现这段代码负责维护EnvDTE的调整。

    例如,自定义MSBuild任务可能被视为"内置"解决方案,但这些自定义任务对我办公室的普通开发人员完全隐藏。 (前/后构建事件也是临界隐藏的,但至少它们位于GUI中,开发人员有时可能偶然遇到这些事件

  3. 那么,有没有人知道NuGet包( StudioShell可能是完美的,但看起来该项目已被放弃,并且不支持VS2015 )或者其他一些解决方案,可以让我从前/后构建过程中访问绑定到当前VS实例的DTE对象?或者更好的是,在加载解决方案/项目时使用EnvDTE调整内容的方法?

    具体调整

    我试图从EnvDTE调整的具体设置在我看来并不是真正相关,但是因为我知道有人会问,这是我目前正在尝试在解决方案中实现的调整/项目加载或至少在构建/调试过程开始之前。

    1. 设置启动项目 - 我很惊讶这个bug仍然困扰着我,但尽管已经在解决方案中设置了启动项目,但不可避免的是其他一些开发人员解决了我的解决方案并最终将错误的项目设置为启动项目,该项目与依赖项/构建顺序混淆并打破构建。

    2. 配置用户/环境特定项目设置(即存储在<project>.user文件中的设置),这些设置已知通用,足以供所有构建环境使用。

      更具体地说,我想在Web项目中设置Project Properties -> Web -> Start Action配置项。对于普通的Web项目,我理解为什么这些设置是特定于用户的,因为谁知道dev可能具有哪些浏览器。但是,我有一个Web托管的WCF服务,其测试脚本嵌套在项目目录下 - 因此,即使解决方案/项目的路径在环境之间发生变化,也应该始终能够使用构建时已知的相对路径运行这些脚本来完成项目调试

3 个答案:

答案 0 :(得分:1)

以下是我提出的构建后事件PowerShell解决方案(针对Restricted执行策略中运行的用户的解决方法是在此答案的最后)。


1)创建包含以下内容的PowerShell脚本:

Param(
    [Parameter(Mandatory=$True,Position=1)]
    [string] $projPath,
    [Parameter(Mandatory=$True,Position=2)]
    [string] $debugScriptPath
)

# Setup new data types and .NET classes to get EnvDTE from a specific process
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text.RegularExpressions;

public static class NTDLL
{
    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_BASIC_INFORMATION
    {
        public IntPtr Reserved1;
        public IntPtr PebBaseAddress;
        public IntPtr Reserved2_0;
        public IntPtr Reserved2_1;
        public IntPtr UniqueProcessId;
        public UIntPtr ParentUniqueProcessId;
    }

    public static UInt32 GetParentProcessID(IntPtr handle)
    {
        PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION();
        Int32 returnLength;
        UInt32 status = NtQueryInformationProcess(handle, IntPtr.Zero, ref pbi, Marshal.SizeOf(pbi), out returnLength);

        if (status != 0)
            return 0;

        return pbi.ParentUniqueProcessId.ToUInt32();
    }

    [DllImport("ntdll.dll")]
    private static extern UInt32 NtQueryInformationProcess(IntPtr processHandle, IntPtr processInformationClass, ref PROCESS_BASIC_INFORMATION processInformation, Int32 processInformationLength, out Int32 returnLength);
}

public static class OLE32
{
    [DllImport("ole32.dll")]
    public static extern Int32 CreateBindCtx(UInt32 reserved, out IBindCtx ppbc);
}

public class VisualStudioProcBinder
{
    public static Object GetDTE(int processId)
    {
        Regex VSMonikerNameRegex = new Regex(@"!?VisualStudio\.DTE([\.\d]+)?:" + processId);

        object runningObject = null;

        IBindCtx bindCtx = null;
        IRunningObjectTable rot = null;
        IEnumMoniker enumMonikers = null;

        try
        {
            Marshal.ThrowExceptionForHR(OLE32.CreateBindCtx(0, out bindCtx));
            bindCtx.GetRunningObjectTable(out rot);
            rot.EnumRunning(out enumMonikers);

            IMoniker[] moniker = new IMoniker[1];
            IntPtr numberFetched = IntPtr.Zero;
            while (enumMonikers.Next(1, moniker, numberFetched) == 0)
            {
                IMoniker runningObjectMoniker = moniker[0];

                string name = null;

                try
                {
                    if (runningObjectMoniker != null)
                        runningObjectMoniker.GetDisplayName(bindCtx, null, out name);
                }
                catch (UnauthorizedAccessException)
                {
                    // Do nothing, there is something in the ROT that we do not have access to.
                }

                if (!string.IsNullOrEmpty(name) && VSMonikerNameRegex.IsMatch(name))
                {
                    Marshal.ThrowExceptionForHR(rot.GetObject(runningObjectMoniker, out runningObject));
                    break;
                }
            }
        }
        finally
        {
            if (enumMonikers != null)
                Marshal.ReleaseComObject(enumMonikers);

            if (rot != null)
                Marshal.ReleaseComObject(rot);

            if (bindCtx != null)
                Marshal.ReleaseComObject(bindCtx);
        }

        return runningObject;
    }
}
"@


# Get the devenv.exe process that started this pre/post-build event
[Diagnostics.Process] $dteProc = [Diagnostics.Process]::GetCurrentProcess();

while ($dteProc -and $dteProc.MainModule.ModuleName -ne 'devenv.exe')
{
    #Write-Host "$(${dteProc}.Id) = $(${dteProc}.MainModule.ModuleName)";
    try { $dteProc = [Diagnostics.Process]::GetProcessById([NTDLL]::GetParentProcessID($dteProc.Handle)); }
    catch { $_; $dteProc = $null; }
}

# Get dteCOMObject using the parent process we just located
$dteCOMObject = [VisualStudioProcBinder]::GetDTE($dteProc.Id);

# Get the project directory
$projDir = Split-Path $projPath -Parent;

# If the script path does not exist on its own - try using it relative to the project directory
if (!(Test-Path $debugScriptPath)) {
    $debugScriptPath = "${projDir}\${debugScriptPath}";
}

#####################################################
# Finally, tweak the project
#####################################################
if ($dteCOMObject) {

    # Get the project reference from DTE
    $dteProject = $dteCOMObject.Solution.Projects | ? { $_.FileName -eq $projPath } | Select-Object -First 1;

    # Set this project as the startup project
    $startupProj = $dteCOMObject.Solution.Properties["StartupProject"].Value;

    if ($startupProj -ne $dteProject.Name) {
        $dteCOMObject.Solution.Properties["StartupProject"].Value = $dteProject.Name;
    }

    # Get the external debug program and arguments currently in use
    $debugProg = $dteProject.Properties['WebApplication.StartExternalProgram'].Value;
    $debugArgs = $dteProject.Properties['WebApplication.StartCmdLineArguments'].Value;

    # If an external debug program is not set, or it is set to cmd.exe /C "<file path>"
    # and "file path" points to a file that doesn't exist (ie. project path has changed)
    # then correct the program/args
    if (!$debugProg -or ($debugProg -eq $env:ComSpec -and $debugArgs -match '^\s*/C\s+("?)([^"]+)\1$'-and !(Test-Path $Matches[2]))) {
        if (!$debugProg) { $dteProject.Properties['WebApplication.DebugStartAction'].Value = 2;  } # 2 = run external program

        $dteProject.Properties['WebApplication.StartExternalProgram'].Value = $env:ComSpec; # run cmd.exe

        # pass "<project dir>\Testing\Debug.cmd" as the program to run from cmd.exe
        $dteProject.Properties['WebApplication.StartCmdLineArguments'].Value = "/C `"${debugScriptPath}`"";
    }

    # Release our COM object reference
    [Runtime.InteropServices.Marshal]::ReleaseComObject($dteCOMObject) | Out-Null;
}


2)从构建后的项目中调用PowerShell脚本,如:

powershell.exe -File "$(ProjectDir)script.ps1" "$(ProjectPath)" "$(ProjectDir)Testing\Debug.cmd"

第一个参数(在-File之后)是您在步骤1中创建的脚本的路径,第二个参数是正在构建的项目的路径,第三个参数(您的脚本可能没有)除非你试图完全按照我的意思行事,否则是配置为在使用外部程序进行调试时运行的批处理文件/脚本的路径。

仅限于在Restricted执行政策

下运行的用户的解决方法

(即。powershell.exe -Command "Set-ExecutionPolicy Unrestricted"不起作用)


如果PowerShell被锁定在Restricted执行策略中,您将无法使用powershell.exe -File script.ps1命令或通过powershell.exe -Command ". .\script.ps1"等点源方法运行PowerShell脚本。但是,我发现您可以将脚本读入变量,然后运行Invoke-Expression $ScriptContent。 (对我来说,这似乎很奇怪,但确实如此)

解决方法包括:

1)使用上面相同的内容创建PowerShell脚本,但排除顶部的Param(...)行。

2)从项目后期构建中调用PowerShell脚本,如:

powershell -Command "& { $projPath='$(ProjectPath)'; $debugScriptPath='$(ProjectDir)Testing\Debug.cmd'; Get-Content '$(ProjectDir)script.ps1' -Encoding String | ? { $_ -match '^^\s*[^^#].*$' } | %% { $VSTweaks += $_ + """`r`n"""; }; Invoke-Expression $VSTweaks; } }"

这会将script.ps1的内容读入名为$VSTweaks的变量(跳过仅作为注释的行 - 即在某些情况下会导致问题的数字签名行),然后使用{运行脚本内容{1}}。通过原始脚本中的参数传递到Invoke-Expression$projPath的值现在在$debugScriptPath中的调用开始时设置。 (如果未设置则脚本将失败)

注意:因为VS项目的构建后事件内容是作为Windows批处理文件执行的,所以您必须转义许多Windows批处理文件特有的字符。这解释了脚本中一些令人困惑的字符组合

  • %= %%
  • ^ = ^^
  • &#34; =&#34;&#34;&#34; (老实说,我不确定为什么这是必需的,但似乎是)

提示

我已经开始在try catch语句中包装整个PowerShell调用(在下面的示例中为powershell.exe -Command "& { $projPath ...}),以便在Visual Studio&#34;错误列表中显示任何PowerShell错误。 &#34;在构建失败时查看。

<PowerShell commands>


VS中生成的错误消息如下所示:

post-build PowerShell errors in Visual Studio Error List

答案 1 :(得分:0)

您可以使用以下代码运行VBScript文件(.vbs):

Dim dte
Set dte = GetObject(, "VisualStudio.DTE")
dte.ExecuteCommand("Help.About")

答案 2 :(得分:0)

在我尝试找到解决方案的过程中,我简要介绍了如何创建自己的.NET PowerShell cmdlet,它将为启动PowerShell脚本的VS实例返回EnvDTE.DTE对象。

代码是丑陋的,但是它起作用并且它在大多数情况下满足了我的要求(在最后的注释中更多)。因此,我认为我会继续将其作为答案包含在内,以防万一有人更喜欢这种方法或需要一个起点来制作自己的DTE cmdlet。

用法示例:

PS C:\> Import-Module .\GetDTECmdlet.dll;
PS C:\> $dte = Get-DTE | Select-Object -First 1;
PS C:\> $dte = Get-DTE -ProcID 8547 | Select-Object -First 1;
PS C:\> $dte = Get-DTE -FromAncestorProcs | Select-Object -First 1;
PS C:\> $dte.ExecuteCommand('Help.About');
PS C:\> [Runtime.InteropServices.Marshal]::ReleaseComObject($dte); | Out-Null;


Cmdlet参数:

-ProcID&lt; int&gt; :int =正在运行的Visual Studio实例的进程ID(PID),以便从中获取DTE

-FromAncestorProcs :如果指定了此开关,则Get-DTE会将其对DTE对象的搜索限制为进程树中较高的Visual Studio进程(即父级/祖先进程)调用它的PowerShell会话。


源代码:

(确保您的项目引用System.Management.Automation.dllenvdte.dll

using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

namespace VSAutomation
{
    [Cmdlet(VerbsCommon.Get, "DTE")]
    [OutputType(typeof(EnvDTE.DTE))]
    public class GetDTECmdlet : Cmdlet, IDisposable
    {
        private Int32 procID = -1;
        private IBindCtx bindCtx = null;
        private IRunningObjectTable rot = null;
        private IEnumMoniker monikerEnumerator = null;
        private IMoniker[] moniker = new IMoniker[1];
        private ProcCollection matchingProcs = new ProcCollection();

        [Parameter]
        public SwitchParameter FromAncestorProcs { get; set; }

        [Parameter]
        public Int32 ProcID { get { return procID; } set { procID = value; } }

        protected override void BeginProcessing()
        {
            base.BeginProcessing();

            Marshal.ThrowExceptionForHR(OLE32.CreateBindCtx(0, out bindCtx));
            bindCtx.GetRunningObjectTable(out rot);
            rot.EnumRunning(out monikerEnumerator);
        }

        protected override void ProcessRecord()
        {
            base.ProcessRecord();

            Regex VSMonikerNameRegex = new Regex(@"^!?VisualStudio\.DTE([\.\d]+)?:(?<PID>\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

            // Get a list of ancestor PIDs if the results should be limited based on ancestor processes
            if (FromAncestorProcs.IsPresent)
            {
                try
                {
                    using (Process thisProc = Process.GetCurrentProcess())
                    {
                        Process proc = thisProc;
                        Int32 parentProcID;

                        while ((parentProcID = NTDLL.GetParentProcessID(proc.Handle)) != 0)
                        {
                            proc = Process.GetProcessById(parentProcID);
                            matchingProcs.Add(new ROTProc(proc));
                        }
                    }
                }
                catch { }
            }

            // Loop through the running objects and find a suitable DTE
            while (monikerEnumerator.Next(1, moniker, IntPtr.Zero) == 0)
            {
                Object runningObject;
                IMoniker runningObjectMoniker = moniker[0];

                if (!FromAncestorProcs.IsPresent && ProcID == -1)
                {
                    // Returning all DTE objects from running processes

                    //Only return each object once
                    if (!matchingProcs.Contains(runningObjectMoniker))
                    {
                        Marshal.ThrowExceptionForHR(rot.GetObject(runningObjectMoniker, out runningObject));
                        EnvDTE.DTE dte = runningObject as EnvDTE.DTE;

                        if (dte != null && !matchingProcs.Contains(dte))
                        {
                            matchingProcs.Add(new ROTProc(dte, runningObjectMoniker));
                            WriteObject(runningObject);
                        }
                    }

                    continue;
                }


                // Returning only DTE objects from ancestor processes or a specific process
                Match nameMatch;
                String name = null;

                try
                {
                    if (runningObjectMoniker != null)
                        runningObjectMoniker.GetDisplayName(bindCtx, null, out name);
                }
                catch (UnauthorizedAccessException)
                {
                    // Do nothing, there is something in the ROT that we do not have access to.
                }

                if (String.IsNullOrEmpty(name))
                    continue;

                nameMatch = VSMonikerNameRegex.Match(name);

                if (!nameMatch.Success)
                    continue;

                if (ProcID != -1)
                {
                    if (Int32.Parse(nameMatch.Groups["PID"].Value) != ProcID)
                        continue;

                    //Found a match for the specified process ID - send it to the pipeline and quit enumerating
                    Marshal.ThrowExceptionForHR(rot.GetObject(runningObjectMoniker, out runningObject));
                    if (runningObject is EnvDTE.DTE)
                    {
                        WriteObject(runningObject);
                        return;
                    }
                }

                // collect DTE objects so that they can be returned in order from closest ancestor to farthest ancestor in the event that VS launched VS which launched MSBUild ...
                ROTProc ancestorProc = matchingProcs.GetByProcId(Int32.Parse(nameMatch.Groups["PID"].Value));

                if (ancestorProc == null)
                    continue;

                Marshal.ThrowExceptionForHR(rot.GetObject(runningObjectMoniker, out runningObject));
                ancestorProc.DTE = runningObject as EnvDTE.DTE;
            }

            if (!FromAncestorProcs.IsPresent)
                return;

            for (Int32 i = 0; i < matchingProcs.Count; i++)
                if (matchingProcs[i].DTE != null) WriteObject(matchingProcs[i].DTE);
        }

        protected override void EndProcessing()
        {
            base.EndProcessing();
            Dispose();
        }

        protected override void StopProcessing()
        {
            base.StopProcessing();
            Dispose();
        }

        public void Dispose()
        {
            if (monikerEnumerator != null)
            {
                Marshal.ReleaseComObject(monikerEnumerator);
                monikerEnumerator = null;
            }

            if (rot != null)
            {
                Marshal.ReleaseComObject(rot);
                rot = null;
            }

            if (bindCtx != null)
            {
                Marshal.ReleaseComObject(bindCtx);
                bindCtx = null;
            }

            if (matchingProcs != null)
            {
                matchingProcs.Dispose();            
                matchingProcs = null;
            }
        }

        private class ROTProc : IDisposable
        {
            public Process Proc = null;
            public EnvDTE.DTE DTE = null;
            public IMoniker Moniker = null;
            public IntPtr COMPtr = IntPtr.Zero;

            public ROTProc(Process Proc, EnvDTE.DTE DTE = null, IMoniker Moniker = null)
            {
                this.Proc = Proc;
                this.DTE = DTE;
                this.Moniker = Moniker;

                if (DTE != null)
                    COMPtr = Marshal.GetComInterfaceForObject(DTE, typeof(EnvDTE._DTE));
            }

            public ROTProc(EnvDTE.DTE DTE, IMoniker Moniker) : this(null, DTE, Moniker) { }

            public void Dispose()
            {
                if (Proc != null)
                {
                    try { Proc.Dispose(); }
                    catch (ObjectDisposedException) { }

                    Proc = null;
                }

                if (COMPtr != IntPtr.Zero)
                {
                    try { Marshal.Release(COMPtr); }
                    catch { }

                    COMPtr = IntPtr.Zero;
                }
            }
        }

        private class ProcCollection : System.Collections.CollectionBase, IDisposable
        {
            public ROTProc this[Int32 index]
            {
                get { return InnerList[index] as ROTProc; }
                set { InnerList[index] = value; }
            }

            public Int32 Add(ROTProc p)
            {
                return InnerList.Add(p);
            }

            public Boolean Contains(IMoniker Moniker)
            {
                if (Moniker == null)
                    return false;

                foreach (ROTProc p in this)
                    if (p != null && Moniker.IsEqual(p.Moniker) == 0) return true;

                return false;
            }

            public Boolean Contains(EnvDTE.DTE DTE)
            {
                if (DTE == null)
                    return false;


                foreach (ROTProc p in this)
                {
                    if (p != null && (
                        Marshal.Equals(DTE, p.DTE) ||
                        Marshal.GetComInterfaceForObject(DTE, typeof(EnvDTE._DTE)) == p.COMPtr))
                    {
                        return true;
                    }
                }


                return false;
            }

            public ROTProc GetByProcId(Int32 ProcId)
            {
                foreach (ROTProc p in this)
                    if (p != null && p.Proc != null && p.Proc.Id == ProcId) return p;

                return null;
            }

            public void Dispose()
            {
                foreach (ROTProc p in this)
                {
                    try { if (p != null) p.Dispose(); }
                    catch (ObjectDisposedException) { }
                }
            }
        }
    }

    #region Supporting interop classes

    public static class OLE32
    {
        [DllImport("ole32.dll")]
        public static extern Int32 CreateBindCtx(UInt32 reserved, out IBindCtx ppbc);
    }

    public static class NTDLL
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct PROCESS_BASIC_INFORMATION
        {
            public IntPtr Reserved1;
            public IntPtr PebBaseAddress;
            public IntPtr Reserved2_0;
            public IntPtr Reserved2_1;
            public IntPtr UniqueProcessId;
            public IntPtr ParentUniqueProcessId;
        }

        public static Int32 GetParentProcessID(IntPtr handle)
        {
            PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION();
            Int32 returnLength;
            UInt32 status = NtQueryInformationProcess(handle, IntPtr.Zero, ref pbi, Marshal.SizeOf(pbi), out returnLength);

            if (status != 0)
                return 0;

            return pbi.ParentUniqueProcessId.ToInt32();
        }

        [DllImport("ntdll.dll")]
        private static extern UInt32 NtQueryInformationProcess(IntPtr processHandle, IntPtr processInformationClass, ref PROCESS_BASIC_INFORMATION processInformation, Int32 processInformationLength, out Int32 returnLength);
    }

    #endregion Supporting interop classes
}


注意:我仍然更喜欢单一的PowerShell脚本解决方案(发布in another answer)。但这主要是因为在我的环境中强制执行的Restricted execution policy。因此,即使我使用此cmdlet来完成获取DTE引用的繁重工作,我的其余PowerShell代码也必须压缩成一个丑陋的,批量转义的,难以理解的单行代码(即{ {1}})。

即使在我写作的时候,我也很难理解。所以,我知道如果他们不得不调整它,任何未来的开发人员都会诅咒我。

在尝试找到解决“丑陋的单行”问题的方法时,我发现了使用Get-ContentInvoke-Expression来“伪造”PowerShell脚本的方法。一旦我执行了该例程,我决定使用单个PowerShell脚本而不是项目来创建cmdlet 使用它的PowerShell脚本。