gists, aspnet, csharp, dotnet comments edit

While working on solving a CR_Documentor known issue, I realized I needed to have some sort of embedded web server running so I could serve up dynamically generated content. I didn’t need a full ASP.NET stack, just something I could pipe a string to and have it serve that up so a hosted IE control would be getting content from a “live server” rather than from a file on disk or from in-memory DOM manipulation… because both of those latter methods cause security warnings to pop up.

I looked at creating a dependency on the ASP.NET development server, WebDev.WebServer.exe, or the assembly that contains the guts of that, WebDev.WebHost.dll, but both of those only get installed in certain configurations of Visual Studio (I think it’s when you install the Visual Web Developer portion) and I couldn’t really assume everyone had that. Paulo Morgado then pointed me to HttpListener, and let me tell you, that’s a pretty sweet solution.

Here’s a very simple web server implementation that uses HttpListener. You handle an event, provide some content for the incoming request, and that’s what the response content is. It doesn’t read files from the filesystem, it doesn’t do auth, it doesn’t do ASP.NET… it’s just the simplest of simple servers, which is exactly what I need for CR_Documentor.

UPDATE 2/2/2009: I found that the super-simple way I had things caused some interesting and unfortunate race conditions which meant things occasionally locked up for no particular reason. As such, I’ve updated the code sample to use events to handle incoming requests and show the listener running on a separate thread. It’s still pretty simple, all things considered, and I have it up and running in CR_Documentor.

using System;
using System.Globalization;
using System.Net;
using System.Threading;

namespace HttpListenerExample
{
  public class WebServer : IDisposable
  {
    public event EventHandler<HttpRequestEventArgs> IncomingRequest = null;

    public enum State
    {
      Stopped,
      Stopping,
      Starting,
      Started
    }

    private Thread _connectionManagerThread = null;
    private bool _disposed = false;
    private HttpListener _listener = null;
    private long _runState = (long)State.Stopped;

    public State RunState
    {
      get
      {
        return (State)Interlocked.Read(ref _runState);
      }
    }

    public virtual Guid UniqueId { get; private set; }

    public virtual Uri Url { get; private set; }

    public WebServer(Uri listenerPrefix)
    {
      if (!HttpListener.IsSupported)
      {
        throw new NotSupportedException("The HttpListener class is not supported on this operating system.");
      }
      if(listenerPrefix == null)
      {
        throw new ArgumentNullException("listenerPrefix");
      }
      this.UniqueId = Guid.NewGuid();
      this._listener = new HttpListener();
      this._listener.Prefixes.Add(listenerPrefix.AbsoluteUri);
    }

    ~WebServer()
    {
      this.Dispose(false);
    }

    private void ConnectionManagerThreadStart()
    {
      Interlocked.Exchange(ref this._runState, (long)State.Starting);
      try
      {
        if (!this._listener.IsListening)
        {
          this._listener.Start();
        }
        if (this._listener.IsListening)
        {
          Interlocked.Exchange(ref this._runState, (long)State.Started);
        }

        try
        {
          while (RunState == State.Started)
          {
            HttpListenerContext context = this._listener.GetContext();
            this.RaiseIncomingRequest(context);
          }
        }
        catch (HttpListenerException)
        {
          // This will occur when the listener gets shut down.
          // Just swallow it and move on.
        }
      }
      finally
      {
        Interlocked.Exchange(ref this._runState, (long)State.Stopped);
      }
    }

    public virtual void Dispose()
    {
      this.Dispose(true);
      GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
      if (this._disposed)
      {
        return;
      }
      if (disposing)
      {
        if (this.RunState != State.Stopped)
        {
          this.Stop();
        }
        if (this._connectionManagerThread != null)
        {
          this._connectionManagerThread.Abort();
          this._connectionManagerThread = null;
        }
      }
      this._disposed = true;
    }

    private void RaiseIncomingRequest(HttpListenerContext context)
    {
      HttpRequestEventArgs e = new HttpRequestEventArgs(context);
      try
      {
        if (this.IncomingRequest != null)
        {
          this.IncomingRequest.BeginInvoke(this, e, null, null);
        }
      }
      catch
      {
        // Swallow the exception and/or log it, but you probably don't want to exit
        // just because an incoming request handler failed.
      }
    }

    public virtual void Start()
    {
      if (this._connectionManagerThread == null || this._connectionManagerThread.ThreadState == ThreadState.Stopped)
      {
        this._connectionManagerThread = new Thread(new ThreadStart(this.ConnectionManagerThreadStart));
        this._connectionManagerThread.Name = String.Format(CultureInfo.InvariantCulture, "ConnectionManager_{0}", this.UniqueId);
      }
      else if (this._connectionManagerThread.ThreadState == ThreadState.Running)
      {
        throw new ThreadStateException("The request handling process is already running.");
      }

      if (this._connectionManagerThread.ThreadState != ThreadState.Unstarted)
      {
        throw new ThreadStateException("The request handling process was not properly initialized so it could not be started.");
      }
      this._connectionManagerThread.Start();

      long waitTime = DateTime.Now.Ticks + TimeSpan.TicksPerSecond * 10;
      while (this.RunState != State.Started)
      {
        Thread.Sleep(100);
        if (DateTime.Now.Ticks > waitTime)
        {
          throw new TimeoutException("Unable to start the request handling process.");
        }
      }
    }

    public virtual void Stop()
    {
      // Setting the runstate to something other than "started" and
      // stopping the listener should abort the AddIncomingRequestToQueue
      // method and allow the ConnectionManagerThreadStart sequence to
      // end, which sets the RunState to Stopped.
      Interlocked.Exchange(ref this._runState, (long)State.Stopping);
      if (this._listener.IsListening)
      {
        this._listener.Stop();
      }
      long waitTime = DateTime.Now.Ticks + TimeSpan.TicksPerSecond * 10;
      while (this.RunState != State.Stopped)
      {
        Thread.Sleep(100);
        if (DateTime.Now.Ticks > waitTime)
        {
          throw new TimeoutException("Unable to stop the web server process.");
        }
      }

      this._connectionManagerThread = null;
    }
  }

  public class HttpRequestEventArgs : EventArgs
  {
    public HttpListenerContext RequestContext { get; private set; }

    public HttpRequestEventArgs(HttpListenerContext requestContext)
    {
      this.RequestContext = requestContext;
    }
  }
}

With this simple wrapper, you can new-up a web server instance, start it listening for requests, and handle the IncomingRequest event to serve up the content you want. Dispose the instance and you’re done. Here’s what it looks like in a simple console program host:

using System;
using System.IO;
using System.Net;
using System.Text;

namespace HttpListenerExample
{
  class Program
  {
    static void Main(string[] args)
    {
      const string ServerTestUrl = "http://localhost:11235/";
      using (WebServer listener = new WebServer(new Uri(ServerTestUrl)))
      {
        listener.IncomingRequest += WebServer_IncomingRequest;
        listener.Start();
        Console.WriteLine("Listener accepting requests: {0}", listener.RunState == WebServer.State.Started);
        Console.WriteLine("Making requests...");
        for(int i = 0; i < 10; i ++)
        {
          HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ServerTestUrl);
          HttpWebResponse response = (HttpWebResponse)request.GetResponse();
          using(Stream responseStream = response.GetResponseStream())
          using(StreamReader responseStreamReader = new StreamReader(responseStream, Encoding.UTF8))
          {
            Console.WriteLine(responseStreamReader.ReadToEnd());
          }
          System.Threading.Thread.Sleep(1000);
        }

      }
      Console.WriteLine("Done. Press any key to exit.");
      Console.ReadLine();
    }

    public static void WebServer_IncomingRequest(object sender, HttpRequestEventArgs e)
    {
      HttpListenerResponse response = e.RequestContext.Response;
      string content = DateTime.Now.ToLongTimeString();
      byte[] buffer = Encoding.UTF8.GetBytes(content);
      response.StatusCode = (int)HttpStatusCode.OK;
      response.StatusDescription = "OK";
      response.ContentLength64 = buffer.Length;
      response.ContentEncoding = Encoding.UTF8;
      response.OutputStream.Write(buffer, 0, buffer.Length);
      response.OutputStream.Close();
      response.Close();
    }
  }
}

I’m using this approach in CR_Documentor to serve up the content preview and maybe augment it a bit later so I can also serve images from embedded resources and such, making the preview that much richer and more accurate.

Saturday was my 12th treatment on my face, and the last in my second set of six (they sell treatments in blocks of six).

A lot has changed since the first treatment, so since I re-upped for a third set of six, I thought it would be time for a retrospective.

  • It still hurts, but it’s nothing like the first time I went. I get MeDioStar on my entire face now, and it’s not nearly as bad because the hair is so much thinner. Reading back on that first entry, I totally remember how much apprehension I felt before going back for the second treatment. I don’t feel that anymore. (There really is no way you can prepare someone for it, so I think a lot of my reaction was that my pain-related expectations were vastly different than reality. Whattaya gonna do?)
  • I still really like all of the people at the clinic. Everyone from the folks at the front desk to the technicians and the sales people are super nice. They all remember my name, they all make me feel totally welcome, and they’re super easy to get along with. I’m glad I chose them.
  • I thought I’d be a closer to done than I am, but then, I was supposed to have done 12 MeDioStar treatments by now and I haven’t. I spent a lot of time doing just Dermo Flash to thin things out. It was the right thing to do, but it’s taking a while.
  • I really like the results so far. My hair grows differently on my face - I pretty much have to shave against the grain to get a good shave - but I’ve also not destroyed any sheets, pillowcases, or shirts for quite some time.
  • The spots that are being stubborn are my chin, upper lip, a spot on my right cheek, and a spot on my neck. The rest has done really well. Even the stubborn spots are starting to give way, it’s just taking a while.

So I did my thing, got my second set of six, and I’m crusin’ along. I should probably take some before and after pix in a couple of weeks to see how well this next set of treatments goes.

dotnet, vs comments edit

There’s a known issue with the latest CR_Documentor

  • sometimes, on an unpredictable basis, it’ll start issuing that “active content” JavaScript security warning. It does that because we’re updating the document by poking data into the DOM directly. Usually setting your IE security settings to “allow active content to run in files on My Computer” fixes it, but not always.

Unfortunately, it’s not really something I can replicate easily, but I know the fix is to, well, stop doing that dynamic poking thing and just serve it up like a regular web server. I have a couple of options:

  1. Create a custom in-proc web server from scratch. I’d have the most control over it and the least dependencies, but it’s the most amount of work, too.
  2. Add a dependency to the ASP.NET development server and use that. Basically, just fire up the ASP.NET development server and serve from a temporary filesystem location.

Is it safe to assume most folks have the ASP.NET development server installed with Visual Studio? I could detect if it was installed and issue an error when the user displays the window to tell them they need to have it installed. I’m thinking writing web servers, however tiny, is not my “core competency” so I’d rather use something off the shelf than try to roll everything myself.

UPDATE: Turns out rolling your own is easy with HttpListener. I’m going to try that first.

UPDATE 2:This is currently being worked on and should be fixed in the next version. You can follow the issue progress on the CR_Documentor site.

UPDATE 3: It’s fixed! Go get it!

downloads, vs, coderush comments edit

I know it’s bad news to release things on Friday right before the day is out, but I can’t hold it in any longer:

I am pleased to announce that after a far-too-long silence, CR_Documentor 2.0.0.0 has been released and is now available for download.

Three major things to report in this release:

  1. All reported bugs have been fixed. If you mailed me or left a comment about a bug, it should be resolved in this release. While there are some known issues, things should render right and it should behave itself reasonably.
  2. Sandcastle “Prototype” preview style is available. You can choose between the classic NDoc preview style or the new Sandcastle “Prototype” style.
  3. The plugin is now open source. I’ve created a new home for the plugin on Google Code and have released it under the Apache 2.0 license. I’d love to get some help and contributions on it, but even if you’re just curious about how it works under the hood, feel free to grab the code.

All the info - FAQ, known issues, etc. - is all on the new CR_Documentor site at Google Code, as is the download. Head on over and check it out!

downloads, vs, coderush comments edit

My CR_JoinLines and CR_SortLines plugins for DXCore have been joined up with the DXCore Community Plugins project headed by Rory Becker. Complete source, installation/usage info, etc., has all been put up over there and any further dev on them will be done there.

Hopefully this more public release area will be helpful to folks who not only want to find and use the plugins, but also who want to learn how to write plugins.