Debugging a Windows Service application without installation

You cannot run a windows service project on your development machine by pressing F5, at least not out of the box. If you did, you would get this error message.

Cannot start service from the command line or a debugger.
A Windows Service must first be installed (using installutil.exe)
and then started with the ServerExplorer, Windows Services
Administrative tool or the NET START command

But you can use conditional compilation to make your application a windows forms application in certain build configurations.

Create the Windows Service project

I started by creating a new Windows service project in Visual Studio 2012. This definitely works in 2008 and 2010 as I have tried it years ago, and it should work in any other prior version of Visual Studio for .Net. The Program.cs will end up with something like the following.

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    static void Main()
    {
        ServiceBase[] ServicesToRun;
        ServicesToRun = new ServiceBase[] { new CandorWorkerRoleService() };
        ServiceBase.Run(ServicesToRun);
    }
}

The only difference will be the name of your service class. It defaults to Service1. I recommend you delete Service1 and create a new windows service item with a name that makes sense for your needs.

Minimize code in the windows service

The first step you can take is to take as much logic out of your windows service project as possible and put it in a class library. This also has the benefit of facilitating unit tests to execute your service logic.

My service class is really simple. It just passes the start and stop method calls off to a class in a class library to do the work. You can see the WorkerRole class on candor common at github. This class is partial because the other half is in a designer file generated when you create a Windows Service item template.

partial class CandorWorkerRoleService : ServiceBase
{
    private WorkerRole workerRole_;
    public CandorWorkerRoleService()
    {
        InitializeComponent();
    }

    protected override void OnStart(string[] args)
    {
        var tasks = new ProviderCollection(typeof(WorkerRole));
        workerRole_ = new WorkerRole(tasks);
        workerRole_.OnStart();
    }
#if DEBUG
	protected virtual void OnStop( string[] args )
	{
		OnStop();
	}
#endif
    protected override void OnStop()
    {
        if (workerRole_ != null)
            workerRole_.OnStop();
    }
}

This class has a conditional compilation to add in an OnStop taking in a string array of arguments for use by the test form. It is not needed by the windows service code, so it is excluded from the release build.

Conditional Startup – The solution



You can change the type of your windows service application at startup by changing the default Program.cs file. I do this with conditional compilation. Swap out the static Main entry point method from using ServiceBase.Run (windows service) to using Application.Run (windows forms). For the code to compile you will also need to reference the System.Windows.Forms framework assembly in your windows service project. Make sure you are using the same service class in both blocks of code. In the example below it is CandorWorkerRoleService.  See the whole windows service project on github.

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    static void Main()
    {
#if DEBUG
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new ServiceTestForm(new CandorWorkerRoleService()));
#else
        ServiceBase[] ServicesToRun;
        ServicesToRun = new ServiceBase[] { new CandorWorkerRoleService() };
        ServiceBase.Run(ServicesToRun);
#endif
    }
}

Creating a test form

The example code above assumes you have a windows form defined called ‘ServiceTestForm’.  You can see the latest ServiceTestForm class on github. You need to build this to do whatever you want. All it needs to do is take in a ServiceBase type in the constructor and hook up stop and start buttons to the corresponding methods on that ServiceBase instance. You can find the latest code for this on candor common at github. Here is the code as of this writing.

using System;
using System.Windows.Forms;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
using Microsoft.Win32;
using System.Timers;
using System.Threading;
using System.IO;
using System.Security;

namespace Candor.Tasks.MultiWorkerService
{
#if DEBUG
    public partial class ServiceTestForm : Form
    {
        ServiceBase service = null;

        public ServiceTestForm()
        {
            InitializeComponent();
        }

        #region State
        /// <summary>
        /// Since we can't have a ServiceController,
        /// determine our own state.
        /// </summary>
        enum State
        {
            Stopped,
            Running,
            Paused
        }
        State state = State.Stopped;
        /// <summary>
        /// Gets or sets service state. Allows start/stop/pause.
        /// </summary>
        State ServState
        {
            get { return state; }
            set
            {
                switch (value)
                {
                    case State.Paused:
                        if (state == State.Running)
                            InvokeServiceMember("OnPause");
                        else
                        {
                            pause.Enabled = false;
                            throw new ApplicationException("Can't pause unless running.");
                        }
                        break;
                    case State.Running:
                        if (state == State.Stopped)
                            InvokeServiceMember("OnStart", new string[] { "" });
                        else if (state == State.Paused)
                            InvokeServiceMember("OnContinue");
                        else
                            throw new ApplicationException("Can't start unless stopped.");
                        pause.Text = "Pause";
                        break;
                    case State.Stopped:
                        InvokeServiceMember("OnStop");
                        break;
                }
                state = value;
            }
        }
        #endregion

        /// <summary>
        /// Create a test form for the given service.
        /// </summary>
        /// <param name="serv"> Instance of a ServiceBase derivation. </param>
        public ServiceTestForm(ServiceBase serv)
        {
            service = serv;
            InitializeComponent();
        }
        void InvokeServiceMember(string name)
        {
            InvokeServiceMember(name, null);
        }
        void InvokeServiceMember(string name, object args)
        {
            InvokeServiceMember(name, new object[] { args });
        }
        void InvokeServiceMember(string name, object[] args)
        {
            Type serviceType = service.GetType();
            serviceType.InvokeMember(name,
            System.Reflection.BindingFlags.Instance
            | System.Reflection.BindingFlags.InvokeMethod
            | System.Reflection.BindingFlags.NonPublic
            | System.Reflection.BindingFlags.Public,
            null,
            service,
            new object[] { args });
        }

        #region Event Handlers
        private void start_Click(object sender, EventArgs e)
        {
            try
            {
                this.Enabled = false;
                ServState = State.Running;
                output.Text = "Started";
                start.Enabled = false;
                stop.Enabled = true;
                pause.Enabled = true;
            }
            catch (Exception ex)
            {
                output.Text = ex.Message + "\r\n";
            }
            finally
            {
                this.Enabled = true;
            }
        }
        private void stop_Click(object sender, EventArgs e)
        {
            try
            {
                this.Enabled = false;
                ServState = State.Stopped;
                output.Text = "Stopped";
                start.Enabled = true;
                stop.Enabled = false;
                pause.Enabled = false;
            }
            catch (Exception ex)
            {
                output.Text = ex.Message + "\r\n";
            }
            finally
            {
                this.Enabled = true;
            }
        }
        private void pause_Click(object sender, EventArgs e)
        {
            try
            {
                this.Enabled = false;
                if (ServState == State.Paused)
                {
                    ServState = State.Running;
                    pause.Text = "Pause";
                    output.Text = "Resumed";
                }
                else if (ServState == State.Running)
                {
                    ServState = State.Paused;
                    pause.Text = "Continue";
                    output.Text = "Paused";
                }
                stop.Enabled = true;
                start.Enabled = false;
            }
            catch (Exception ex)
            {
                output.Text = ex.Message + "\r\n";
            }
            finally
            {
                this.Enabled = true;
            }
        }
        private void sendCommand_Click(object sender, EventArgs e)
        {
            try
            {
                InvokeServiceMember("OnCustomCommand", (int)command.Value);
            }
            catch (Exception ex)
            {
                output.Text = ex.Message + "\r\n";
            }
        }
        #endregion
    }
#endif
}

And the backing designer file defining the form layout is as follows.

namespace Candor.Tasks.MultiWorkerService
{
#if DEBUG
    partial class ServiceTestForm
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;
        private System.Windows.Forms.GroupBox groupBox1;
        private System.Windows.Forms.Button start;
        private System.Windows.Forms.Button stop;
        private System.Windows.Forms.NumericUpDown command;
        private System.Windows.Forms.Button sendCommand;
        private System.Windows.Forms.Button pause;
        private System.Windows.Forms.GroupBox groupBox2;
        private System.Windows.Forms.TextBox output;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing &amp;amp;&amp;amp; (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.groupBox1 = new System.Windows.Forms.GroupBox();
            this.start = new System.Windows.Forms.Button();
            this.pause = new System.Windows.Forms.Button();
            this.stop = new System.Windows.Forms.Button();
            this.command = new System.Windows.Forms.NumericUpDown();
            this.sendCommand = new System.Windows.Forms.Button();
            this.groupBox2 = new System.Windows.Forms.GroupBox();
            this.output = new System.Windows.Forms.TextBox();
            this.groupBox1.SuspendLayout();
            ((System.ComponentModel.ISupportInitialize)(this.command)).BeginInit();
            this.groupBox2.SuspendLayout();
            this.SuspendLayout();
            // 
            // groupBox1
            // 
            this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
                        | System.Windows.Forms.AnchorStyles.Right)));
            this.groupBox1.Controls.Add(this.start);
            this.groupBox1.Controls.Add(this.pause);
            this.groupBox1.Controls.Add(this.stop);
            this.groupBox1.Location = new System.Drawing.Point(12, 12);
            this.groupBox1.Name = "groupBox1";
            this.groupBox1.Size = new System.Drawing.Size(251, 50);
            this.groupBox1.TabIndex = 5;
            this.groupBox1.TabStop = false;
            this.groupBox1.Text = "State";
            // 
            // start
            // 
            this.start.Location = new System.Drawing.Point(6, 19);
            this.start.Name = "start";
            this.start.Size = new System.Drawing.Size(75, 23);
            this.start.TabIndex = 0;
            this.start.Text = "Start";
            this.start.UseVisualStyleBackColor = true;
            this.start.Click += new System.EventHandler(this.start_Click);
            // 
            // pause
            // 
            this.pause.Enabled = false;
            this.pause.Location = new System.Drawing.Point(168, 19);
            this.pause.Name = "pause";
            this.pause.Size = new System.Drawing.Size(75, 23);
            this.pause.TabIndex = 4;
            this.pause.Text = "Pause";
            this.pause.UseVisualStyleBackColor = true;
            this.pause.Click += new System.EventHandler(this.pause_Click);
            // 
            // stop
            // 
            this.stop.Enabled = false;
            this.stop.Location = new System.Drawing.Point(87, 19);
            this.stop.Name = "stop";
            this.stop.Size = new System.Drawing.Size(75, 23);
            this.stop.TabIndex = 1;
            this.stop.Text = "Stop";
            this.stop.UseVisualStyleBackColor = true;
            this.stop.Click += new System.EventHandler(this.stop_Click);
            // 
            // command
            // 
            this.command.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
                        | System.Windows.Forms.AnchorStyles.Right)));
            this.command.Location = new System.Drawing.Point(128, 20);
            this.command.Maximum = new decimal(new int[] {
            2147483647,
            0,
            0,
            0});
            this.command.Name = "command";
            this.command.Size = new System.Drawing.Size(115, 20);
            this.command.TabIndex = 2;
            this.command.Value = new decimal(new int[] {
            129,
            0,
            0,
            0});
            // 
            // sendCommand
            // 
            this.sendCommand.Location = new System.Drawing.Point(6, 19);
            this.sendCommand.Name = "sendCommand";
            this.sendCommand.Size = new System.Drawing.Size(116, 23);
            this.sendCommand.TabIndex = 3;
            this.sendCommand.Text = "Send Command";
            this.sendCommand.UseVisualStyleBackColor = true;
            this.sendCommand.Click += new System.EventHandler(this.sendCommand_Click);
            // 
            // groupBox2
            // 
            this.groupBox2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
                        | System.Windows.Forms.AnchorStyles.Right)));
            this.groupBox2.Controls.Add(this.sendCommand);
            this.groupBox2.Controls.Add(this.command);
            this.groupBox2.Location = new System.Drawing.Point(13, 68);
            this.groupBox2.Name = "groupBox2";
            this.groupBox2.Size = new System.Drawing.Size(250, 51);
            this.groupBox2.TabIndex = 0;
            this.groupBox2.TabStop = false;
            this.groupBox2.Text = "Custom Commands";
            // 
            // output
            // 
            this.output.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
                        | System.Windows.Forms.AnchorStyles.Left)
                        | System.Windows.Forms.AnchorStyles.Right)));
            this.output.Location = new System.Drawing.Point(12, 125);
            this.output.Multiline = true;
            this.output.Name = "output";
            this.output.Size = new System.Drawing.Size(251, 66);
            this.output.TabIndex = 6;
            // 
            // ServiceTestForm
            // 
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
            this.AutoSize = true;
            this.ClientSize = new System.Drawing.Size(279, 203);
            this.Controls.Add(this.output);
            this.Controls.Add(this.groupBox2);
            this.Controls.Add(this.groupBox1);
            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
            this.Name = "ServiceTestForm";
            this.Text = "ServiceTestForm";
            this.groupBox1.ResumeLayout(false);
            ((System.ComponentModel.ISupportInitialize)(this.command)).EndInit();
            this.groupBox2.ResumeLayout(false);
            this.ResumeLayout(false);
            this.PerformLayout();

        }
        #endregion
    }
#endif
}

This form allows you to press a button to stop and start the service and see how it responds to those commands in the Services management interface. Before pressing start, just place your breakpoint in your service class explained in the previous section. You shouldn’t need to modify this form code.

References

Development trunk of candor-common on Github containing all the code above
https://github.com/michael-lang/candor-common

For an old school windows services debugging comparison…
Windows Services in C#: Debugging Windows Services (Part 4)
http://arcanecode.com/2007/05/24/windows-services-in-c-debugging-windows-services-part-4/

About the Author

Michael Lang

Co-Founder and CTO of Watchdog Creative, business development, technology vision, and more since 2013, Developer, and Mentor since 1999. See the about page for more details.

2 Comments

  • Avatar By Paul G Wichtendahl

    Thank you thank you thank you!