// /* ********************************************************************************** // * // * Copyright (c) Sky Sanders. All rights reserved. // * // * This source code is subject to terms and conditions of the Microsoft Public // * License (Ms-PL). A copy of the license can be found in the license.htm file // * included in this distribution. // * // * You must not remove this notice, or any other, from this software. // * // * **********************************************************************************/ using System; using System.Diagnostics; using System.IO; using System.Net; using System.Threading; using Salient.SqlServer.Testing; namespace CassiniDev.Testing.SqlServer { /// /// Made a go at spinning the server up from this process but after dealing with /// irratic behaviour regarding apartment state, platform concerns, unloaded app domains, /// and all the other issues that you can find that people struggle with I just decided /// to strictly format the console app's output and just spin up an external process. /// Seems robust so far. /// public partial class CassiniSqlFixture : DatabaseFixture, IDisposable { private bool _disposed; private bool _hostAdded; private string _hostname; private StreamWriter _input; private IPAddress _ipAddress; private Thread _outputThread; private string _rootUrl; private Process _serverProcess; public CassiniSqlFixture(string dataSource, string initialCatalog) : base(dataSource, initialCatalog) { } public CassiniSqlFixture(string dataSource, string initialCatalog, string userId, string password) : base(dataSource, initialCatalog, userId, password) { } public CassiniSqlFixture(string connectionString) : base(connectionString) { } /// /// The root URL of the running web application /// public string RootUrl { get { return _rootUrl; } } /// /// Combine the RootUrl of the running web application with the relative url /// specified. /// /// /// public virtual Uri NormalizeUri(string relativeUrl) { relativeUrl = relativeUrl.TrimStart(new[] {'/'}); string rootUrl = _rootUrl; if (!rootUrl.EndsWith("/")) { rootUrl += "/"; } return new Uri(rootUrl + relativeUrl); } /// /// Finds first available port in range on specified IP address. /// To check a specific port set start and end to same value. /// /// /// /// /// /// If no port in range is available. public static ushort GetPort(ushort portRangeStart, ushort portRangeEnd, IPAddress ipAddress) { ushort port = ServiceFactory.Rules.GetAvailablePort(portRangeStart, portRangeEnd, ipAddress, true); if (port == 0) { throw new InvalidOperationException("Port is in use"); } return port; } /// /// /// Physical path to application. /// IP to listen on. /// Port to listen on. /// Optional. default value '/' /// Optional unless addHostsEntry is true. In all cases is used to construct RootUrl. /// If true, add hosts file entry. Requires read/write permissions to hosts file. /// Length of time, in ms, to wait for a specific port before throwing an exception or exiting. 0 = don't wait. /// Length of time, in ms, to wait for a request before stopping the server. 0 = no timeout. /// If true attempts to add a temporary entry to windows hosts file comprised of ipAddress and hostname. /// The entry is removed when server is stopped. /// Throws UnauthorizedAccessException if process does not have write permissions to hosts file. /// public virtual void StartServer(string applicationPath, IPAddress ipAddress, ushort port, string virtualPath, string hostname, bool addHostsEntry, int waitForPort, int timeOut) { _hostAdded = addHostsEntry; _hostname = hostname; _ipAddress = ipAddress; // massage and validate arguments if (string.IsNullOrEmpty(virtualPath)) { virtualPath = "/"; } if (!virtualPath.StartsWith("/")) { virtualPath = "/" + virtualPath; } if (_serverProcess != null) { throw new InvalidOperationException("Server is running"); } if (addHostsEntry) { if (string.IsNullOrEmpty(hostname)) { throw new InvalidOperationException("Hostname is missing"); } ServiceFactory.Rules.AddHostEntry(_ipAddress.ToString(), _hostname); } // use the arg object to construct command line string string commandLine = (new CommandLineArguments { Port = port, Path = string.Format("\"{0}\"", Path.GetFullPath(applicationPath).Trim('\"').TrimEnd('\\')), HostName = hostname, IPAddress = ipAddress.ToString(), VirtualPath = string.Format("\"{0}\"",virtualPath), TimeOut = timeOut, WaitForPort = waitForPort, IPMode=IPMode.Specific, PortMode=PortMode.Specific }).ToString(); _serverProcess = new Process(); _serverProcess.StartInfo = new ProcessStartInfo { UseShellExecute = false, ErrorDialog = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardInput = true, FileName = "CassiniDev-console.exe", Arguments = commandLine, WorkingDirectory = Environment.CurrentDirectory }; // we are going to monitor each line of the output until we get a start or error signal // and then just ignore the rest string line = null; _serverProcess.Start(); _outputThread = new Thread(() => { string l = _serverProcess.StandardOutput.ReadLine(); while (l != null) { if (l.StartsWith("started:") || l.StartsWith("error:")) { line = l; } l = _serverProcess.StandardOutput.ReadLine(); } }); _outputThread.Start(); // use StandardInput to send the newline to stop the server when required _input = _serverProcess.StandardInput; // block until we get a signal while (line == null) { Thread.Sleep(10); } if (!line.StartsWith("started:")) { throw new Exception(string.Format("Could not start server: {0}", line)); } // line is the root url _rootUrl = line.Substring(line.IndexOf(':') + 1); } /// /// Stops the server, if running and removes hosts entry if added. /// public virtual void StopServer() { StopServer(100); } /// /// Stops the server, if running and removes hosts entry if added. /// public virtual void StopServer(int delay) { Thread.Sleep(delay); if (_serverProcess != null) { try { _input.WriteLine(); _serverProcess.WaitForExit(10000); if (_hostAdded) { ServiceFactory.Rules.RemoveHostEntry(_ipAddress.ToString(), _hostname); } Thread.Sleep(10); } catch { } finally { _serverProcess.Dispose(); _serverProcess = null; } } } #region IDisposable public void Dispose() { if (!_disposed) { if (_serverProcess != null) { StopServer(); } } _disposed = true; GC.SuppressFinalize(this); } ~CassiniSqlFixture() { Dispose(); } #endregion } }