以编程方式启动和停止IIS Express

62

我想在C#中构建一个小应用程序,它应该启动/停止IIS Express工作进程。为此,我想使用官方的“ IIS Express API”,该API在MSDN上有文档:http://msdn.microsoft.com/en-us/library/gg418415.aspx

据我所知,该API仅基于COM接口。为了使用这些COM接口,我通过添加引用->COM->“IIS安装版本管理器接口”在VS2010中添加了对COM库的引用:

到此为止还好,但下一步是什么?有一个可用的IIISExprProcessUtility接口,其中包括启动/停止IIS进程的两个“方法”。我是否需要编写实现此接口的类?

public class test : IISVersionManagerLibrary.IIISExprProcessUtility
{
    public string ConstructCommandLine(string bstrSite, string bstrApplication, string bstrApplicationPool, string bstrConfigPath)
    {
        throw new NotImplementedException();
    }

    public uint GetRunningProcessForSite(string bstrSite, string bstrApplication, string bstrApplicationPool, string bstrConfigPath)
    {
        throw new NotImplementedException();
    }

    public void StopProcess(uint dwPid)
    {
        throw new NotImplementedException();
    }
} 

如您所见,我不是专业开发人员。有人能指点我正确的方向吗?非常感谢任何帮助。

更新 1: 根据建议,我尝试了以下代码,但不幸的是它并没有起作用:

alt text 好吧,它可以实例化,但我不知道如何使用这个对象...

alt text

alt text

IISVersionManagerLibrary.IIISExpressProcessUtility test3 = (IISVersionManagerLibrary.IIISExpressProcessUtility) Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("5A081F08-E4FA-45CC-A8EA-5C8A7B51727C")));

Exception: Retrieving the COM class factory for component with CLSID {5A081F08-E4FA-45CC-A8EA-5C8A7B51727C} failed due to the following error: 80040154 Class not registered (Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG)).
10个回答

66

我曾试图做类似的事情,但最终得出结论,微软提供的COM库不完整。我不使用它是因为文档中提到“注意:此主题是预发布文档,可能会在以后的版本中更改。”

所以,我决定看看IISExpressTray.exe在做什么。它似乎在做类似的事情。

我反汇编了IISExpressTray.dll并发现列出所有IISexpress进程并停止IISexpress进程没有任何奇技淫巧。

它没有调用那个COM库,也没有从注册表中查找任何内容。

所以,我最终得出的解决方案非常简单。要启动IIS express进程,我只需使用Process.Start()并传入我需要的所有参数。

要停止IIS express进程,我从Reflector中复制了IISExpressTray.dll中的代码。我看到它只是向目标IISExpress进程发送WM_QUIT消息。

这是我编写的启动和停止IIS express进程的类。希望这能帮助其他人。

class IISExpress
{
    internal class NativeMethods
    {
        // Methods
        [DllImport("user32.dll", SetLastError = true)]
        internal static extern IntPtr GetTopWindow(IntPtr hWnd);
        [DllImport("user32.dll", SetLastError = true)]
        internal static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
        [DllImport("user32.dll", SetLastError = true)]
        internal static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint lpdwProcessId);
        [DllImport("user32.dll", SetLastError = true)]
        internal static extern bool PostMessage(HandleRef hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    }

    public static void SendStopMessageToProcess(int PID)
    {
        try
        {
            for (IntPtr ptr = NativeMethods.GetTopWindow(IntPtr.Zero); ptr != IntPtr.Zero; ptr = NativeMethods.GetWindow(ptr, 2))
            {
                uint num;
                NativeMethods.GetWindowThreadProcessId(ptr, out num);
                if (PID == num)
                {
                    HandleRef hWnd = new HandleRef(null, ptr);
                    NativeMethods.PostMessage(hWnd, 0x12, IntPtr.Zero, IntPtr.Zero);
                    return;
                }
            }
        }
        catch (ArgumentException)
        {
        }
    }

    const string IIS_EXPRESS = @"C:\Program Files\IIS Express\iisexpress.exe";
    const string CONFIG = "config";
    const string SITE = "site";
    const string APP_POOL = "apppool";

    Process process;

    IISExpress(string config, string site, string apppool)
    {
        Config = config;
        Site = site;
        AppPool = apppool;

        StringBuilder arguments = new StringBuilder();
        if (!string.IsNullOrEmpty(Config))
            arguments.AppendFormat("/{0}:{1} ", CONFIG, Config);

        if (!string.IsNullOrEmpty(Site))
            arguments.AppendFormat("/{0}:{1} ", SITE, Site);

        if (!string.IsNullOrEmpty(AppPool))
            arguments.AppendFormat("/{0}:{1} ", APP_POOL, AppPool);

        process = Process.Start(new ProcessStartInfo()
        {
            FileName = IIS_EXPRESS,
            Arguments = arguments.ToString(),
            RedirectStandardOutput = true,
            UseShellExecute = false
        });
    }

    public string Config { get; protected set; }
    public string Site { get; protected set; }
    public string AppPool { get; protected set; }

    public static IISExpress Start(string config, string site, string apppool)
    {
        return new IISExpress(config, site, apppool);
    }

    public void Stop()
    {
        SendStopMessageToProcess(process.Id);
        process.Close();
    }
}

我不需要列出所有现有的IIS Express进程。如果你需要,从我在反编译器中看到的内容来看,IISExpressTray.dll所做的是调用Process.GetProcessByName("iisexpress", ".")

要使用我提供的类,这里有一个我用来测试的示例程序。

class Program
{

    static void Main(string[] args)
    {
        Console.Out.WriteLine("Launching IIS Express...");
        IISExpress iis1 = IISExpress.Start(
            @"C:\Users\Administrator\Documents\IISExpress\config\applicationhost.config",
            @"WebSite1(1)",
            @"Clr4IntegratedAppPool");

        IISExpress iis2 = IISExpress.Start(
            @"C:\Users\Administrator\Documents\IISExpress\config\applicationhost2.config",
            @"WebSite1(1)",
            @"Clr4IntegratedAppPool");

        Console.Out.WriteLine("Press ENTER to kill");
        Console.In.ReadLine();

        iis1.Stop();
        iis2.Stop();
    }
}

这可能不是你问题的答案,但我认为对你问题感兴趣的人可能会发现我的工作很有用。请随意改进代码。以下是一些你可能希望增强的地方:

  1. 而不是硬编码iisexpress.exe的位置,你可以修改我的代码以从注册表中读取。
  2. 我没有包含iisexpress.exe支持的所有参数。
  3. 我没有进行错误处理。所以,如果IISExpress进程由于某些原因(例如端口被占用)启动失败,我将不知道。我认为最简单的解决方法是监视StandardError流,如果我从StandardError流中获得任何东西,则抛出异常

2
哇!非常感谢你提供的详细答案!我觉得只要在MSDN上没有完整/正确的COM文档,这似乎是一个完美的解决方案。我一旦回到电脑前就会立刻尝试它。 - Mike
太好了,停止消息完美地起作用了。我正在从命令行运行IIS Express,但这是缺失的部分。谢谢! - jpierson
1
太棒了!我在Process.Start中添加了CreateNoWindow = true,并在我的测试夹具的SetUp/TearDown期间使用它。非常感谢! - Colin Zabransky
谢谢。我写了类似的代码并且在使用ISS Express时使用了process.Kill(),但由于某些原因它无法正常工作(顺便问一下,有人知道为什么吗?),使用停止消息可以解决问题。 - Alex from Jitbit

15

虽然现在已经有点晚了,但我还是会回答这个问题。

IISVersionManagerLibrary.IISVersionManager mgr = new IISVersionManagerLibrary.IISVersionManagerClass();
IISVersionManagerLibrary.IIISVersion ver = mgr.GetVersionObject("7.5", IISVersionManagerLibrary.IIS_PRODUCT_TYPE.IIS_PRODUCT_EXPRESS);

object obj1 = ver.GetPropertyValue("expressProcessHelper");

IISVersionManagerLibrary.IIISExpressProcessUtility util = obj1 as IISVersionManagerLibrary.IIISExpressProcessUtility;

就是这样。然后你可以在 util 对象上调用 StopProcess 方法。

但是,你必须得到 Microsoft 的通知。

"版本管理器 API(IIS Express); http://msdn.microsoft.com/zh-cn/library/gg418429(v=VS.90).aspx

注意:IIS 版本管理器 API 支持 IIS Express 基础结构, 不应直接从您的代码中使用。 "


谢谢 (+1)。编译器报错并告诉我修改第一行:IISVersionManagerLibrary.IISVersionManager mgr = new IISVersionManagerLibrary.IISVersionManager(); - ilmatte
2
谢谢!这段代码对我来说运行得很好,但我有点不知道接下来该怎么做。我该如何找出它正在运行的端口?我该如何使用它来启动我正在开发的网站? - Dan Csharpster
哎呀,这在最新的.NET(4.5+)中不起作用...错误 CS0656:缺少编译器所需的成员“Microsoft.CSharp.RuntimeBinder.Binder.Convert”。 - David V. Corbin
1
@DavidV.Corbin,你的问题与这个答案无关。去找你的答案吧,一定有的。(顺便说一下,我在.NET 4.6.1中成功编译了它) - SeongTae Jeong
2
永远不晚! - Anton Kalcik

7
这个实现适用于通过编程方式启动/停止IIS Express,可用于测试。
public class IisExpress : IDisposable
{
    private Boolean _isDisposed;

    private Process _process;

    public void Dispose()
    {
        Dispose(true);
    }

    public void Start(String directoryPath, Int32 port)
    {
        var iisExpressPath = DetermineIisExpressPath();
        var arguments = String.Format(
            CultureInfo.InvariantCulture, "/path:\"{0}\" /port:{1}", directoryPath, port);

        var info = new ProcessStartInfo(iisExpressPath)
                                    {
                                        WindowStyle = ProcessWindowStyle.Normal,
                                        ErrorDialog = true,
                                        LoadUserProfile = true,
                                        CreateNoWindow = false,
                                        UseShellExecute = false,
                                        Arguments = arguments
                                    };

        var startThread = new Thread(() => StartIisExpress(info))
                                 {
                                     IsBackground = true
                                 };

        startThread.Start();
    }

    protected virtual void Dispose(Boolean disposing)
    {
        if (_isDisposed)
        {
            return;
        }

        if (disposing)
        {
            if (_process.HasExited == false)
            {
                _process.Kill();
            }

            _process.Dispose();
        }

        _isDisposed = true;
    }

    private static String DetermineIisExpressPath()
    {
        String iisExpressPath;

        iisExpressPath = Environment.GetFolderPath(Environment.Is64BitOperatingSystem 
            ? Environment.SpecialFolder.ProgramFilesX86
            : Environment.SpecialFolder.ProgramFiles);

        iisExpressPath = Path.Combine(iisExpressPath, @"IIS Express\iisexpress.exe");

        return iisExpressPath;
    }

    private void StartIisExpress(ProcessStartInfo info)
    {
        try
        {
            _process = Process.Start(info);

            _process.WaitForExit();
        }
        catch (Exception)
        {
            Dispose();
        }
    }
}

4
太好了!我在这里找到了一个更加完整的实现 here - orad
1
在这里使用 Environment.Is64BitOperatingSystem 是错误的选择。它实际上取决于站点构建的二进制文件的位数。 - jpmc26

3

这是我的解决方案。它使用隐藏窗口运行IIS Express。管理类控制多个IIS Express实例。

class IISExpress
{               
    private const string IIS_EXPRESS = @"C:\Program Files\IIS Express\iisexpress.exe";        

    private Process process;

    IISExpress(Dictionary<string, string> args)
    {
        this.Arguments = new ReadOnlyDictionary<string, string>(args);

        string argumentsInString = args.Keys
            .Where(key => !string.IsNullOrEmpty(key))
            .Select(key => $"/{key}:{args[key]}")
            .Aggregate((agregate, element) => $"{agregate} {element}");

        this.process = Process.Start(new ProcessStartInfo()
        {
            FileName = IIS_EXPRESS,
            Arguments = argumentsInString,
            WindowStyle = ProcessWindowStyle.Hidden                
        });
    }

    public IReadOnlyDictionary<string, string> Arguments { get; protected set; }        

    public static IISExpress Start(Dictionary<string, string> args)
    {
        return new IISExpress(args);
    }

    public void Stop()
    {
        try
        {
            this.process.Kill();
            this.process.WaitForExit();
        }
        finally
        {
            this.process.Close();
        }            
    }        
}

我需要几个实例。设计管理器类来控制它们。

static class IISExpressManager
{
    /// <summary>
    /// All started IIS Express hosts
    /// </summary>
    private static List<IISExpress> hosts = new List<IISExpress>();

    /// <summary>
    /// Start IIS Express hosts according to the config file
    /// </summary>
    public static void StartIfEnabled()
    {
        string enableIISExpress = ConfigurationManager.AppSettings["EnableIISExpress"]; // bool value from config file
        string pathToConfigFile = ConfigurationManager.AppSettings["IISExpressConfigFile"]; // path string to iis configuration file
        string quotedPathToConfigFile = '"' + pathToConfigFile + '"';

        if (bool.TryParse(enableIISExpress, out bool isIISExpressEnabled) 
            && isIISExpressEnabled && File.Exists(pathToConfigFile))
        {                
            hosts.Add(IISExpress.Start(
                new Dictionary<string, string> {
                    {"systray", "false"},
                    {"config", quotedPathToConfigFile},
                    {"site", "Site1" }                        
                }));

            hosts.Add(IISExpress.Start(
                new Dictionary<string, string> {
                    {"systray", "false"},
                    { "config", quotedPathToConfigFile},
                    {"site", "Site2" }
                }));

        }
    }

    /// <summary>
    /// Stop all started hosts
    /// </summary>
    public static void Stop()
    {
        foreach(var h in hosts)
        {
            h.Stop();
        }
    }
}

3

Harvey Kwok提供了一个很好的提示,因为我想在运行集成测试用例时撕毁服务。但是Harvey的代码太长了,涉及到PInvoke和消息传递。

这里有一个替代方案。

    public class IisExpressAgent
{
    public void Start(string arguments)
    {
        ProcessStartInfo info= new ProcessStartInfo(@"C:\Program Files (x86)\IIS Express\iisexpress.exe", arguments)
        {
          // WindowStyle= ProcessWindowStyle.Minimized,
        };

        process = Process.Start(info);
    }

    Process  process;

    public void Stop()
    {
        process.Kill();
    }
}

在我的 MS Test 集成测试套件中,我有

       [ClassInitialize()]
    public static void MyClassInitialize(TestContext testContext)
    {
        iis = new IisExpressAgent();
        iis.Start("/site:\"WcfService1\" /apppool:\"Clr4IntegratedAppPool\"");
    }

    static IisExpressAgent iis;

    //Use ClassCleanup to run code after all tests in a class have run
    [ClassCleanup()]
    public static void MyClassCleanup()
    {
        iis.Stop();
    }

您可能会发现这里没有使用通常的IDisposable模式。然而,处理资源实际上是在ClassCleanup/teardown方法中释放的,并且测试套件进程是短暂的。 - ZZZ

3

Pradeep,感谢你的回答。第一个链接中的解决方案对我不起作用,因为它们使用 proc.Kill() 来结束进程;pinvoke.net 很有趣,谢谢你的提示。不幸的是,我没有找到关于如何使用 IIS Express COM API 的任何信息。 - Mike

2

我觉得我也可以提供我的解决方案。这个方案基于SeongTae Jeong的方案和另一篇文章(现在记不清在哪里了)。

  1. Install the Microsoft.Web.Administration nuget.
  2. Reference the IIS Installed Versions Manager Interface COM type library as noted above.
  3. Add the following class:

    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Text.RegularExpressions;
    using IISVersionManagerLibrary;
    using Microsoft.Web.Administration;
    
    public class Website
    {
        private const string DefaultAppPool = "Clr4IntegratedAppPool";
        private const string DefaultIISVersion = "8.0";
    
        private static readonly Random Random = new Random();
        private readonly IIISExpressProcessUtility _iis;
        private readonly string _name;
        private readonly string _path;
        private readonly int _port;
        private readonly string _appPool;
        private readonly string _iisPath;
        private readonly string _iisArguments;
        private readonly string _iisConfigPath;
        private uint _iisHandle;
    
        private Website(string path, string name, int port, string appPool, string iisVersion)
        {
            _path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, path));
            _name = name;
            _port = port;
            _appPool = appPool;
            _iis = (IIISExpressProcessUtility)new IISVersionManager()
                .GetVersionObject(iisVersion, IIS_PRODUCT_TYPE.IIS_PRODUCT_EXPRESS)
                .GetPropertyValue("expressProcessHelper");
            var commandLine = _iis.ConstructCommandLine(name, "", appPool, "");
            var commandLineParts = new Regex("\\\"(.*?)\\\" (.*)").Match(commandLine);
            _iisPath = commandLineParts.Groups[1].Value;
            _iisArguments = commandLineParts.Groups[2].Value;
            _iisConfigPath = new Regex("\\/config:\\\"(.*?)\\\"").Match(commandLine).Groups[1].Value;
            Url = string.Format("http://localhost:{0}/", _port);
        }
    
        public static Website Create(string path,
            string name = null, int? port = null,
            string appPool = DefaultAppPool,
            string iisVersion = DefaultIISVersion)
        {
            return new Website(path,
                name ?? Guid.NewGuid().ToString("N"),
                port ?? Random.Next(30000, 40000),
                appPool, iisVersion);
        }
    
        public string Url { get; private set; }
    
        public void Start()
        {
            using (var manager = new ServerManager(_iisConfigPath))
            {
                manager.Sites.Add(_name, "http", string.Format("*:{0}:localhost", _port), _path);
                manager.CommitChanges();
            }
            Process.Start(new ProcessStartInfo
            {
                FileName = _iisPath,
                Arguments = _iisArguments,
                RedirectStandardOutput = true,
                UseShellExecute = false
            });
            var startTime = DateTime.Now;
            do
            {
                try
                {
                    _iisHandle = _iis.GetRunningProcessForSite(_name, "", _appPool, "");
                }
                catch { }
                if (_iisHandle != 0) break;
                if ((DateTime.Now - startTime).Seconds >= 10)
                    throw new TimeoutException("Timeout starting IIS Express.");
            } while (true);
        }
    
        public void Stop()
        {
            try
            {
                _iis.StopProcess(_iisHandle);
            }
            finally
            {
                using (var manager = new ServerManager(_iisConfigPath))
                {
                    var site = manager.Sites[_name];
                    manager.Sites.Remove(site);
                    manager.CommitChanges();
                }
            }
        }
    }
    
  4. Setup your test fixture as follows. The path is relative to the bin folder of your test suite.

    [TestFixture]
    public class Tests
    {
        private Website _website;
    
        [TestFixtureSetUp]
        public void Setup()
        {
            _website = Website.Create(@"..\..\..\TestHarness");
            _website.Start();
        }
    
        [TestFixtureTearDown]
        public void TearDown()
        {
            _website.Stop();
        }
    
        [Test]
        public void should_serialize_with_bender()
        {
            new WebClient().UploadString(_website.Url, "hai").ShouldEqual("hai");
        }
    }
    

如果这个程序也要在一个构建服务器上运行,还需要注意一点。首先你需要在构建服务器上安装 IIS Express。其次,你需要在构建服务器上创建一个 applicationhost.config 文件。你可以从你的开发机器上复制一个,路径为 C:\Users\<User>\Documents\IISExpress\config\。该文件需要复制到构建服务器运行用户对应的路径下,如果它是以系统身份运行,则路径将为 C:\Windows\System32\config\systemprofile\Documents\IISExpress\config\


1

我采用了不同的解决方案。您可以使用“taskkill”和进程名称来简单地终止进程树。在本地和TFS 2013上都可以完美运行。

public static void FinalizeIis()
{
    var startInfo = new ProcessStartInfo
    {
        UseShellExecute = false,
        Arguments = string.Format("/F /IM iisexpress.exe"),
        FileName = "taskkill"
    };

    Process.Start(startInfo);
}

1
不,你不能继承接口。你可以使用new关键字创建一个IISVersionManager实例。如何获得IIISExpressProcessUtility实例的引用完全不清楚。MSDN文档很糟糕。也许你可以new一个,但它似乎不支持这样做。

如果您想了解C++编译器如何在接口上工作,请阅读https://dev59.com/SXNA5IYBdhLWcg3wGJwV。 - Pradeep
1
它是COM特定的,所有方法都通过接口公开。 - Hans Passant
非常感谢您的支持!我进行了一些测试并更新了我的帖子。 - Mike
2
是的,很抱歉,但我之前已经警告过你了。恐怕你已经远远领先于曲线。糟糕的MSDN文档实际上是一个重要的信号,微软可以雇用的那些程序员们大概知道自动化接口很重要,但不知道如何实现。COM是胡子和肚子都有的人才会做的事情。它的文档记录非常差,因为它根本不起作用。你要避免的流行语是"CTP"和"Express"。 - Hans Passant
1
感谢您一直以来的支持。然而,由于IIS Express现在已经发布,我仍然希望微软能够在不久的将来完成/修正MSDN上的COM文档。 - Mike

1
如果您修改Web应用程序的web.config文件,IIS(包括Express)将重新启动应用程序池。这将允许您部署更新的程序集。
修改web.config的一种方法是将其复制到新文件中,然后再移回来。
copy /Y path/web.config path/web_touch.config
move /Y path/web_touch.config path/web.config

如果你只需要重启应用程序池,那么这个方法就足够了。但是如果你希望对IIS Express进行更多的控制,那么你需要使用其他方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接