Search code examples
c#debuggingservicevisual-studio-2017

Debugging a service via IDE


I have created a program that can also be ran as a service and it will allow me to debug it as well using the following in the Program.cs startup file.

using System;
using System.Linq;
using System.Windows.Forms;
using System.ServiceProcess;
using System.Reflection;
using System.Threading;
using crs.Includes;
using crs.Service;
using System.IO;

namespace crs
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            //Convert all arguments to lower
            args = Array.ConvertAll(args, e => e.ToLower());

            //Create the container object for the settings to be stored
            Settings.Bag = new SettingsBag();

            //Check if we want to run this as a service
            bool runAsService = args.Contains("-service");

            //Check if debugging
            bool debug = Environment.UserInteractive;

            //Catch all unhandled exceptions as well
            if (!debug || debug)
            {
                Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
                AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
            }

            if (runAsService)
            {
                //Create service array
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[]
                {
                    new CRSService()
                };

                //Run services in interactive mode if needed
                if (debug)
                    RunInteractive(ServicesToRun);
                else
                    ServiceBase.Run(ServicesToRun);
            }
            else
            {
                //Start the main gui
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new MainGUI());
            }
        }

        #region Functions
        private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            Exception ex = e.ExceptionObject as Exception;

            string stackTrace = ex.Message + "/n";
            while (ex.InnerException != null)
            {
                ex = ex.InnerException;

                stackTrace += ex.Message + "/n";
            }
            stackTrace = stackTrace.Substring(0, stackTrace.Length - 2);

            string msg = "UNHANDLED EXCEPTION!/n/n" + stackTrace;

            //Write all log messages to a debug log
            try
            {
                string currentDate = DateTime.Now.ToString("yyyy-MM-dd");
                string debugFilePath = AppDomain.CurrentDomain.BaseDirectory + @"debugLogs\";
                string debugFilename = Application.ProductName + "-debug-" + currentDate + ".log";

                if (!Directory.Exists(debugFilePath))
                {
                    //Create the debug log files directory
                    Directory.CreateDirectory(debugFilePath);
                }
                if (!File.Exists(debugFilePath + debugFilename))
                {
                    //Create the new file
                    using (StreamWriter w = File.CreateText(debugFilePath + debugFilename))
                    {
                        w.WriteLine("Debug log file for " + Application.ProductName + ".");
                        w.WriteLine("Created on " + currentDate + ".");
                        w.WriteLine("");
                    }
                }

                //Write the log message to the file
                using (StreamWriter w = File.AppendText(debugFilePath + debugFilename))
                {
                    w.WriteLine(DateTime.Now.ToString() + " :: " + msg);
                }
            }
            catch
            { }
        }

        private static void RunInteractive(ServiceBase[] servicesToRun)
        {
            Console.WriteLine("Services running in interactive mode.");
            Console.WriteLine();

            MethodInfo onStartMethod = typeof(ServiceBase).GetMethod("OnStart", BindingFlags.Instance | BindingFlags.NonPublic);
            foreach (ServiceBase service in servicesToRun)
            {
                Console.Write("Starting {0}...", service.ServiceName);
                onStartMethod.Invoke(service, new object[] { new string[] { } });
                Console.Write("Started");
            }

            Console.WriteLine();
            Console.WriteLine();
            Console.WriteLine("Press any key to stop the services and end the process...");
            Console.ReadKey();
            Console.WriteLine();

            MethodInfo onStopMethod = typeof(ServiceBase).GetMethod("OnStop", BindingFlags.Instance | BindingFlags.NonPublic);
            foreach (ServiceBase service in servicesToRun)
            {
                Console.Write("Stopping {0}...", service.ServiceName);
                onStopMethod.Invoke(service, null);
                Console.WriteLine("Stopped");
            }

            //Keep the console alive for a second to allow the user to see the message.
            Console.WriteLine("All services stopped.");
            Thread.Sleep(1000);
        }
        #endregion
    }
}

Everything works as expected except for the line Console.ReadKey(); under the RunInteractive() method. If I was to try and run this service manually in a console window I would have no issues what so ever, it runs great and waits for me to hit enter to start the service stopping process. However, when running it in the IDE it's spitting everything out to the DEBUG window and there is nothing for it to grab a ReadKey on.

How can I go about getting around this when debugging in the IDE? Is it possible to somehow force it to run in a command window when debugging in the IDE?


Solution

  • After some digging around I came up with a new class to suit my needs. Thanks for the post by Pavlo I was able to actually get text to read and write to the new console window I needed to create when one was not present.

    My altered RunInteractive function from my original question:

    private static void RunInteractive(ServiceBase[] servicesToRun)
    {
        //Account for running this application without a console window (debugging in IDE)
        if (!ConsoleWindow.Exists() && !ConsoleWindow.Create())
            return;
    
        Console.WriteLine("Services running in interactive mode.");
        Console.WriteLine();
    
        MethodInfo onStartMethod = typeof(ServiceBase).GetMethod("OnStart", BindingFlags.Instance | BindingFlags.NonPublic);
        foreach (ServiceBase service in servicesToRun)
        {
            Console.Write("Starting {0}...", service.ServiceName);
            onStartMethod.Invoke(service, new object[] { new string[] { } });
            Console.Write("Started");
        }
    
        Console.WriteLine();
        Console.WriteLine();
        Console.WriteLine("Press any key to stop the services and end the process...");
        Console.ReadKey();
        Console.WriteLine();
    
        MethodInfo onStopMethod = typeof(ServiceBase).GetMethod("OnStop", BindingFlags.Instance | BindingFlags.NonPublic);
        foreach (ServiceBase service in servicesToRun)
        {
            Console.Write("Stopping {0}...", service.ServiceName);
            onStopMethod.Invoke(service, null);
            Console.WriteLine("Stopped");
        }
    
        //Keep the console alive for a second to allow the user to see the message.
        Console.WriteLine("All services stopped.");
        Thread.Sleep(1000);
    }
    

    Note: The only thing that was added here was this little bit at the top of the function.

    //Account for running this application without a console window (debugging in IDE)
    if (!ConsoleWindow.Exists() && !ConsoleWindow.Create())
      return;
    

    My new ConsoleWindow class:

    using System;
    using System.IO;
    using System.Runtime.InteropServices;
    using Microsoft.Win32.SafeHandles;
    
    namespace crs.Includes
    {
        public class ConsoleWindow
        {
            #region Constants
            private const UInt32 GENERIC_WRITE = 0x40000000;
            private const UInt32 GENERIC_READ = 0x80000000;
            private const UInt32 FILE_SHARE_READ = 0x00000001;
            private const UInt32 FILE_SHARE_WRITE = 0x00000002;
            private const UInt32 OPEN_EXISTING = 0x00000003;
            private const UInt32 FILE_ATTRIBUTE_NORMAL = 0x80;
            #endregion
    
            #region WinAPI external functions
            [DllImport("kernel32.dll")]
            private static extern IntPtr GetConsoleWindow();
    
            [DllImport(
                "kernel32.dll",
                SetLastError = true
                )]
            [return: MarshalAs(UnmanagedType.Bool)]
            private static extern bool FreeConsole();
    
            [DllImport(
                "kernel32.dll",
                SetLastError = true
                )]
            [return: MarshalAs(UnmanagedType.Bool)]
            private static extern bool AllocConsole();
    
            [DllImport(
                "kernel32.dll",
                EntryPoint = "CreateFileW",
                SetLastError = true,
                CharSet = CharSet.Auto,
                CallingConvention = CallingConvention.StdCall
                )]
            private static extern IntPtr CreateFileW(
                string lpFileName,
                UInt32 dwDesiredAccess,
                UInt32 dwShareMode,
                IntPtr lpSecurityAttributes,
                UInt32 dwCreationDisposition,
                UInt32 dwFlagsAndAttributes,
                IntPtr hTemplateFil
                );
            #endregion
    
            #region Public class methods
            public static bool Exists()
            {
                if (GetConsoleWindow() == IntPtr.Zero)
                    return false;
                else
                    return true;
            }
    
            public static bool Create()
            {
                try
                {
                    if (!AllocConsole())
                        throw new Exception("Error!  Could not get a lock on a console window and could not create one.");
    
                    InitializeOutStream();
                    InitializeInStream();
    
                    return true;
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
    
                return false;
            }
            #endregion
    
            #region Functions
            private static void InitializeOutStream()
            {
                FileStream fs = CreateFileStream("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE, FileAccess.Write);
                if (fs != null)
                {
                    StreamWriter writer = new StreamWriter(fs) { AutoFlush = true };
                    Console.SetOut(writer);
                    Console.SetError(writer);
                }
            }
    
            private static void InitializeInStream()
            {
                FileStream fs = CreateFileStream("CONIN$", GENERIC_READ, FILE_SHARE_READ, FileAccess.Read);
                if (fs != null)
                    Console.SetIn(new StreamReader(fs));
            }
    
            private static FileStream CreateFileStream(string name, uint win32DesiredAccess, uint win32ShareMode, FileAccess dotNetFileAccess)
            {
                SafeFileHandle file = new SafeFileHandle(CreateFileW(name, win32DesiredAccess, win32ShareMode, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero), true);
                if (!file.IsInvalid)
                {
                    FileStream fs = new FileStream(file, dotNetFileAccess);
                    return fs;
                }
                return null;
            }
            #endregion
        }
    }