概要
Windowsのサービスを開発する際に問題になるのがデバッグのやりにくさだろう。
GUIアプリの場合はF5キー(または開始ボタン)を押せば簡単にデバッグ実行できるが、
サービスの場合は普通にF5キーを押しても「コマンドラインやデバッガーからWindowsサービスを開始できません...」のようなエラーが表示されてデバッグ実行できない。
とはいえブレイクポイントやウォッチ式などを使わずにいわゆる"printデバッグ"をするのは非常に大変だ。
ググッて調べたサービスのデバッグ方法をメモしておく。
サービスをコンソールアプリとして実行する
以下の記事などで紹介されているやり方。
ソースコード
using System;
using System.ServiceProcess;
namespace DebuggableService1
{
    public partial class Service1 : ServiceBase
    {
        public Service1()
        {
            InitializeComponent();
        }
        public void StartDebug(string[] args) => OnStart(args);
        public void StopDebug() => OnStop();
        protected override void OnStart(string[] args)
        {
            Console.WriteLine("Service Started.");
        }
        protected override void OnStop()
        {
            Console.WriteLine("Service Stopped.");
        }
    }
}
using System.ServiceProcess;
using System.Threading;
namespace DebuggableService1
{
    internal static class Program
    {
        static void Main()
        {
            var service = new Service1();
            if (System.Environment.UserInteractive)
            {
                var wh = new EventWaitHandle(false, EventResetMode.AutoReset, "StopService");
                service.StartDebug(null);
                wh.WaitOne();
                service.StopDebug();
            }
            else
            {
                ServiceBase.Run(service);
            }
        }
    }
}
$wh = [System.Threading.EventWaitHandle]::OpenExisting("StopService")
$wh.Set()
使い方
- F5キーなどで起動する。
 - 終了させたいときは「Stop-DebugService.ps1」を実行する。
 
説明
- Visual StudioやEXEファイルや直接起動したときに立つフラグ(
System.Environment.UserInteractive)を使ってデバッグかそうでないかを判断し、サービスとして起動するかコンソールアプリとして起動するかを決定する。 - 
publicなメソッドを踏み台にしてサービスのクラスのprotectedなメソッド(OnStart()、OnStop())を呼び出す。 - 「デバッグの停止」でデバッグをぶった切るのに抵抗があったので
EventWaitHandleを使って外部から終了の指示を送信できるようにしてみた。ここではPowerShellを使ってイベントを発生させている。 - 元の記事にはプロジェクトの「出力の種類」を「Windowsアプリケーション」から「コンソールアプリケーション」に変更しろと指示が書いてあるが、そうしなくてもブレイクポイントなどはちゃんと動く。確かに「コンソールアプリケーション」にしないとコンソールが表示されないが「出力」のウィンドウには
Console.Writeline()なども出力されるし、無理して変えなくてもいいかも。 
.NET Framework 4.8以降はSystem.Environment.UserInteractiveは必ずtrueを返してしまうので判定に使えない。Microsoft.Extensions.Hosting.WindowsServicesを使うと同様のことができるようなので下記のリンクを参照してほしい。
c# - How to determine if starting inside a windows service? - Stack Overflow
サービスにデバッガをアタッチする
Microsoftのドキュメントでも説明されている定番のやり方をちょっとアレンジ。
ソースコード
using System.Diagnostics;
using System.ServiceProcess;
using System.Timers;
namespace DebuggableService2
{
    public partial class Service1 : ServiceBase
    {
        Timer _timer;
        public Service1()
        {
            InitializeComponent();
            _timer = new Timer(300);
            _timer.Elapsed += (_, e) => { Debug.WriteLine("{0:HH:mm:ss}", e.SignalTime); };
            _timer.AutoReset = true;
            _timer.Enabled = true;
        }
        protected override void OnStart(string[] args)
        {
            Debug.WriteLine("Service Started.");
        }
        protected override void OnStop()
        {
            Debug.WriteLine("Service Stopped.");
            _timer.Stop();
            _timer.Dispose();
        }
    }
}
using System.ServiceProcess;
namespace DebuggableService2
{
    internal static class Program
    {
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[]
            {
                new Service1()
            };
            ServiceBase.Run(ServicesToRun);
        }
    }
}
$serviceName = "DebuggableService2"
$exe = Get-Item "DebuggableService2.exe"
Write-Host "Installing Service..."
New-Service -Name $serviceName -BinaryPathName $exe.FullName
Write-Host "Starting Service..."
Start-Service -Name $serviceName
Start-Sleep -Second 3
$serviceName = "DebuggableService2"
Write-Host "Stopping Service..."
Stop-Service $serviceName -ErrorAction Ignore
Write-Host "Uninstalling Service..."
$null = sc.exe delete $serviceName
Start-Sleep -Second 3
使い方
- Visual Studioを管理者権限で起動する
 - サービスの起動/終了をするPowerShellスクリプト
Install-Service.ps1とUninstall-Service.ps1をビルドイベントから呼び出すように設定する。
ビルド前イベントif $(ConfigurationName) == Debug powershell -File "..\..\Script\Uninstall-Service.ps1"ビルド後イベントif $(ConfigurationName) == Debug powershell -File "..\..\Script\Install-Service.ps1" - ビルド または リビルドをする。ソースに変更がない場合はビルドではビルドイベントが走らないので適宜リビルドしないとダメ。
 - スクリプトが走ってビルド&サービスのインストールが完了したら「デバッグ」の「プロセスにアタッチ」を選び、「すべてのユーザーのプロセスを表示する」をチェックして実行したサービスを探してアタッチする。
 - デバッガーが起動してデバッグできるようになる。
 
説明
- 公式の資料ではいちいち手作業でインストールしろと書いてあるが、さすがに面倒なのでビルド時に自動的にインストールされるようにした。
 - コンソールアプリとして実行する方法に比べて手数が多くて嫌だけど、ちゃんとサービスに対してデバッグできている感じがするのでこっちのほうが安心かも。
 - 
$(ConfigurationName)のマクロを使ってデバッグビルド時のみサービスのインストール/アンインストールを行っている。 - たまにサービスのアンインストール/インストールがうまくいかない場合がある。Windowsの「サービス」の画面を開いているとダメな気がする。
 - サービスのインストール/アンインストール用のスクリプトを
startを付けて非同期に呼び出しすれば全体のかかる時間を短縮できるかもしれない。 - なんとなくクリティカルなタイミングがあるような気がしてインストール/アンインストール用のスクリプトの最後に
Start-Sleepを入れて適当な待ちを作っているが、なくてもいいかもしれない。 
サービスにデバッガをアタッチする(OnStartメソッド編)
これもMicrosoftのドキュメント受け売り。OnStartメソッドはサービスの起動時に一回だけ実行されるので起動後にアタッチしてもデバッグすることはできない。よってちょっとした細工が必要だ。
前述の「サービスにデバッガをアタッチする」のソースをちょっと修正する。
ソースコード
using System.Diagnostics;
using System.ServiceProcess;
using System.Timers;
namespace DebuggableService2
{
    public partial class Service1 : ServiceBase
    {
        Timer _timer;
        public Service1()
        {
            InitializeComponent();
            _timer = new Timer(300);
            _timer.Elapsed += (_, e) => { Debug.WriteLine("{0:HH:mm:ss}", e.SignalTime); };
            _timer.AutoReset = true;
            _timer.Enabled = true;
        }
        protected override void OnStart(string[] args)
        {
+ #if DEBUG
+           Debugger.Launch();
+ #endif
            Debug.WriteLine("Service Started.");
        }
        protected override void OnStop()
        {
            Debug.WriteLine("Service Stopped.");
            _timer.Stop();
            _timer.Dispose();
        }
    }
}
使い方
- ビルドすると自動的にサービスが起動されるのと同時にデバッガも起動される。
 - 「はい」のボタンを押下するとデバッガの選択画面が出るので適当に選択する。
 - 
Debugger.Launch()の行からステップ実行等が可能になる。 
説明
- OnStart()の先頭に
System.Diagnostics.Debugger.Launch()を入れるだけ。 - Releaseビルドでも
System.Diagnostics.Debugger.Launch()を呼び出すとデバッガの起動が起動しちゃうのでコンパイルディレクティブで呼び出さないようにする。 - プログラムからデバッガを起動するのでアタッチ先のプロセスをいちいち選ばなくていいのはよい。
 
リポジトリ
まるごとソースコードが欲しい方は以下のリンクからどうぞ。
https://github.com/okbear/DebuggableService1