Starting and stopping IIS Express programmatically

2019-01-06 09:14发布

问题:

I am trying to build a small application in C# which should start/stop an IIS Express worker process. For this purpose I want to use the official "IIS Express API" which is documented on MSDN: http://msdn.microsoft.com/en-us/library/gg418415.aspx

As far as I understand, the API is based (only) on COM interfaces. To use this COM interfaces I've added a reference to the COM library in VS2010 via Add Reference -> COM -> "IIS Installed Versions Manager Interface":

So far so good, but what's next? There is an IIISExprProcessUtility interface available which includes the the two "methods" to start/stop an IIS process. Do I have to write a class which implements this interface?

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();
    }
} 

As you can see, I'm not a professional developer. Can someone point me in the right direction. Any help is greatly appreciated.

Update 1: According to the suggestions I've tried the following code which unfortunately doesn't work:

Ok, it can be instantiated but I cannot see how to use this object...

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)).

回答1:

I was trying to do similar thing. I concluded that the COM library provided by Microsoft is incomplete. I don't use it because the doc mentioned that "Note: This topic is pre-release documentation and is subject to change in future releases".

So, I decided to take a look at what IISExpressTray.exe is doing. It seems to be doing similar things.

I disassemble the IISExpressTray.dll and found that there is no magic in listing out all the IISexpress processes and stoping the IISexpress process.

It doesn't call that COM library. It doesn't lookup anything from registry.

So, the solution I ended up is very simple. To start an IIS express process, I just use Process.Start() and pass in all the parameters I need.

To stop an IIS express process, I copied the code from IISExpressTray.dll using reflector. I saw it simply sends a WM_QUIT message to the target IISExpress process.

Here is the class I wrote to start and stop an IIS express process. Hope this can help somebody else.

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();
    }
}

I don't need to list all the existing IIS express process. If you need that, from what I saw in the reflector, what IISExpressTray.dll does is to call Process.GetProcessByName("iisexpress", ".")

To use the class I provided, here is a sample program I used to test it.

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();
    }
}

This may not be an answer to your question but I think people interesting in your question may find my work useful. Feel free to improve the codes. There are some places that you might want to enhance.

  1. Instead of hardcoding the iisexpress.exe location, you can fix my code to read from the registry.
  2. I didn't include all the arguments supported by iisexpress.exe
  3. I didn't do error handling. So, if the IISExpress process failed to start for some reasons (e.g. port is in used), I don't know. I think the easiest way to fix it is to monitor the StandardError stream and throw exception if I get anything from StandardError stream


回答2:

Although, it's too late, I'll provide an answer to this question.

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;

That's it. Then you can call StopProcess method on util object.

However, you have to get notice from Microsoft.

" Version Manager API (IIS Express) ; http://msdn.microsoft.com/en-us/library/gg418429(v=VS.90).aspx

Note: The IIS Version Manager API supports the IIS Express infrastructure and is not intended to be used directly from your code. "



回答3:

This implementation works for starting/stopping IIS Express programmatically, can be used from tests.

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:

I feel you are doing it in a hard way. Take a hint from this question Automatically stop/restart ASP.NET Development Server on Build and see if you can adopt the same process.

Answering your question, I think pinvoke.net might help you. They have lot of examples as well which can help you build your solution.



回答5:

Harvey Kwok had provided a good hint, since I want to tear up and tear down the service when running integration test cases. But Harvey codes is too long with PInvoke and messaging.

Here's an alternative.

    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();
    }
}

And in my integration test suit with MS Test, I have

       [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();
    }


回答6:

No, you don't inherit the interface. You can create an instance of IISVersionManager with the new keyword. How that gets you a reference to an IIISExpressProcessUtility instance is completely unclear. The MSDN docs are awful. Maybe you can new one but it doesn't look like it supports that.



回答7:

If you modify the web.config file of the web application, IIS (including Express) will restart the app pool. This will allow you to deploy updated assemblies.

One way to modify web.config is to copy it to a new file, and then move it back.

copy /Y path/web.config path/web_touch.config
move /Y path/web_touch.config path/web.config

You may want more control over IIS Express than simply restarting the app pool. But if that's all you need, this will work.



回答8:

I have adopted a different solution. You can simply kill the process tree using "taskkill" and the name of the process. This works perfectly locally and on TFS 2013

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

    Process.Start(startInfo);
}


回答9:

Figure I'd throw my solution in here too. Derived from the SeongTae Jeong's solution and another post (Can't remember where now).

  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");
        }
    }
    

And one more point if this is going to also run on a build server. First you will need to install IIS Express on the build server. Second, you'll have to create an applicationhost.config on the build server. You can copy one from your dev box under C:\Users\<User>\Documents\IISExpress\config\. It needs to be copied to the corresponding path of the user your build server is running as. If it is running as system then the path would be C:\Windows\System32\config\systemprofile\Documents\IISExpress\config\.



回答10:

Here is my solution too. It runs IIS Express with hidden windows. Manager class controls several IIS Express instances.

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();
        }            
    }        
}

I need several instances. Designed manager class to control them.

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();
        }
    }
}