🏃‍♂️

C#定石 - サービス制御 - SQL Server

2025/03/04に公開

はじめに

SQL Server 2025 Private Preview が始まっているので、今更ですが、SQL Server 2022 で SQL Server サービスが遅延開始となりましたね。
今回は、SQL Server をサンプルとして、遅延開始、サービスの状態確認/開始待機、サービスの開始/停止について記載します。

素材

アイコン素材として下記サイトを利用させて頂きました。

テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。

  • Windows Forms - .NET Framework 4.8
  • Windows Forms - .NET 8
  • WPF - .NET Framework 4.8
  • WPF - .NET 8

サービス制御

ServiceController

https://learn.microsoft.com/ja-jp/dotnet/api/system.serviceprocess.servicecontroller

サービス制御は ServiceController を利用します。
事前準備として、下記を行ってください。

  • .NET Framework 4.8
    • ソリューションエクスプローラ[参照][参照の追加]で System.ServiceProcess 追加
    • もしくは、.NET 8 と同様に Nuget で System.ServiceProcess.ServiceController 追加
  • .NET 8 の場合

ServiceController では、「サービス名」を利用します。
サービス コンソールでは「表示名」一覧が表示されます。
対象を選択して、右クリック「プロパティ」を選択すると「サービス名」を確認できます。

遅延開始

サービス コントロール マネージャは、サービスを起動して、所定待機時間(30秒)以内に該当サービスが起動完了しないと、タイムアウトとして処理を中断します。
「スタートアップの種類 - 自動」のサービスが多数存在して、Windows 起動時処理と重なり高負荷状態の場合、このタイムアウト発生の可能性があがるので、直ちに起動しなくてもいいサービスは遅延開始とすることが推奨されています。

SQL Server 2022 から SQL Server サービスが遅延開始となった理由のひとつに、安定性(Windows 起動時の高負荷環境だと、タイムアウトで起動されない可能性があることを排除)があげられています。

https://learn.microsoft.com/ja-jp/sql/sql-server/sql-server-2022-release-notes?view=sql-server-ver16&preserve-view=true#sql-server-services-are-set-to-automatic-delayed-start-start-mode

SQL Server サービスが[自動(遅延開始)]開始モードに設定されている
SQL Server 2022 (16.x) では、SQL Server サービスの[開始モード]を構成マネージャーで[自動]に設定すると、[スタート モード]が[自動]と表示されている場合でも、サービスが代わりに[自動(遅延開始)]モードで開始するように構成されます。

遅延開始の場合、Windows 起動してから待機時間(120秒)経過後にサービス起動を行います。
このため、PC起動 - 自動サインインの環境で、スタートアップなどにソフトを登録した場合、該当ソフト起動時では SQL Server が未起動で利用不可という状態が起こり得ます。

状態確認/開始待機

サービスの状態確認、および、開始待機は、一般ユーザ権限で利用可能です。

ServiceController 生成

SQL Server の場合、サービス名は「MSSQL$インスタンス名」となります。

using System.ServiceProcess;
string instanceName = "Hoge";
string serviceName = $"MSSQL${instanceName}";
var sc = new ServiceController(serviceName, ".");

// ServiceController - System.ComponentModel.Component
//   - MarshalByRefObject, IDisposable, System.ComponentModel.IComponent
// と IDisposable なので、リソース解放 sc.Dispose() が必要です。
// using を利用すると自動的にリソース解放されます。

スタートアップの種類

サービスの開始モードは、ServiceController.StartType で確認できます。

ServiceStartMode startType = sc.StartType;

// ServiceStartMode.Automatic : 自動
// ServiceStartMode.Manual    : 手動
// ServiceStartMode.Disabled  : 無効

しかし、サービスコンソールで表示される「遅延開始」「トリガー開始」に関する情報は、ServiceController からは取得できず、WMI から情報取得、もしくは、レジストリ参照が必要で、双方とも ServiceController.StartType 相当の情報も存在しています。
今回は、サービスコンソールで、スタートアップの種類として表示される「自動(遅延開始、トリガー開始)」形式の文字列を、レジストリ参照で作成するサンプルを記載します。

string instanceName = "Hoge";
var startupKind = GetStartupKind($"MSSQL${instanceName}");
private string GetStartupKind(string serviceName)
{
  string startupKind = string.Empty;

  using (var regKey = 
    Registry.LocalMachine.OpenSubKey($@"SYSTEM\CurrentControlSet\services\{serviceName}"))
  {
    if (regKey != null)
    {
      Int32 start = (Int32)regKey.GetValue("Start", 0); // 2:自動, 3:手動, 4:無効
      Int32 delayed = (Int32)regKey.GetValue("DelayedAutostart", 0); // 1:遅延開始
      bool bTrigger = regKey.GetSubKeyNames().Any(x => x == "TriggerInfo"); // true:トリガー開始

      var sb = new StringBuilder();
      switch(start)
      {
      case 2: sb.Append("自動");
              if (delayed == 1)
              {
                if (bTrigger) sb.Append("(遅延開始、トリガー開始)");
                else          sb.Append("(遅延開始)");
              }
              else if (bTrigger) sb.Append("(トリガー開始)");
              break;
      case 3: sb.Append("手動");
              if (bTrigger) sb.Append("(トリガー開始)");
              break;
      case 4: sb.Append("無効");
              break;
      }
      startupKind = sb.ToString();
    }
  }
  return startupKind;
}

サービスの状態

サービスの状態は、ServiceController.Status で確認できます。

var status = sc.Status;

// ServiceControllerStatus.ContinuePending :  再開中
// ServiceControllerStatus.Paused          :  一時停止
// ServiceControllerStatus.PausePending    :  一時停止中
// ServiceControllerStatus.Running         :  実行中
// ServiceControllerStatus.StartPending    :  開始中
// ServiceControllerStatus.StopPending     :  停止中
// ServiceControllerStatus.Stopped         :  停止

サービスが指定した状態になるまで待機

ServiceController.WaitForStatus を利用すると、サービスが指定したステータスになる、もしくは、指定したタイムアウトに達するまで待機することができます。

本メソッドを利用することで、遅延開始されるサービスが実行中になるまで待機させることが可能です。
数分間の待機となるので、待機をキャンセル可能にすることが望ましいので、C#定石 - ワーキングダイアログ(プログレスダイアログ) を利用したサンプルコードを記載します。

WorkingDialogManager経由で実行
// SQL Server に接続できず、SQL Server サービスが実行中でない場合
// → 実行中になるまでワーキングダイアログで待機、キャンセル可
var result = wdManager.ShowDialog(this, WaitForRunning);
// .NET Framework 時 object? の ? 不要
private void WaitForRunning(object? sender, DoWorkEventArgs e)
{
  string instanceName = "Hoge";
  using (var sc = new ServiceController($"MSSQL${instanceName}", "."))
  {
    if (sc != null 
     && sc.StartType != ServiceStartMode.Disabled     // 無効でない
     && sc.Status != ServiceControllerStatus.Running) // 実行中でない
    {
      // 最大待機時間:3秒間隔 x 80回 = 240秒(4分)
      for (int span = 3, max = 240; max > 0; max -= span)
      {
        // キャンセル確認
        if (wdManager.IsCancelling)
        {
          e.Cancel = true;
          return;
        }
        // ワーキングダイアログ表示更新
        wdManager.SetStepMessage($"SQL Server 起動待機中...\r\n待機残り時間 {max}秒");
        // キャンセル受付可能とするため タイムアウトを 3秒で待機
        try
        {
          // 対象サービスが Running になる、もしくは、3秒でタイムアウトまで待機
          sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(span));
        }
        catch (System.ServiceProcess.TimeoutException)
        {
           // 指定時間内に Running にならない場合の例外
           // 3秒間隔の待機なので無視
           continue;
        }
        // 実行中になった - TODO
        break;
      }
    }
  }
}

開始/停止

サービスの開始、および、停止操作は、管理者権限が必要となります。
まずは、それぞれのサンプルコードを記載します。
末尾に、ユーザ権限の範囲でサービスの状態表示をして、開始/停止は管理者権限で実行するサンプルを記載します。

開始

private void StartService(string serviceName)
{
  int waitSec = 30;  // TODO
  using (var sc = new ServiceController(serviceName, "."))
  {
    if (sc?.Status == ServiceControllerStatus.Stopped   // 停止中
     && sc.StartType != ServiceStartMode.Disabled)      // 無効以外
    {
      // サービス開始
      sc.Start();
      // 対象サービスが Running になる、もしくは、タイムアウトまで待機
      try
      {
        sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(waitSec));
      }
      catch (System.ServiceProcess.TimeoutException)
      {
        // タイムアウト - TODO
      }
    }
  }
}

停止

private void StopService(string serviceName)
{
  int waitSec = 30;  // TODO
  using (var sc = new ServiceController(serviceName, "."))
  {
    if (sc?.Status == ServiceControllerStatus.Running)  // 実行中
    {
      if (sc.CanStop)
      {
        // サービス停止
        sc.Stop();
        // 対象サービスが Stopped になる、もしくは、タイムアウトまで待機
        try
        {
          sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(waitSec));
        }
        catch (System.ServiceProcess.TimeoutException)
        {
          // タイムアウト - TODO
        }
      }
    }
  }
}

管理者権限でサービス開始

一般ユーザ権限で、スタートアップの種類 / サービスの状態を表示して、サービス開始 / サービス停止は管理者権限で実施するサンプルとして、下記レイアウトを用意します。

コントール 内容
TextBox txtStartupKind スタートアップ種類
TextBox txtStatus サービスの状態
Button btnRefresh 状態更新
Button btnStart サービス開始
Button btnStop サービス停止
Button btnExit 終了

「サービス開始」「サービス停止」ボタンは、管理者権限が必要なので、盾マークを付与しています。

サービス開始(サービス停止)を管理者権限で実行する手法として、今回は、下記実装とします。

  • 管理者権限で実行されている場合
    • そのままサービス開始(サービス停止)
  • 管理者権限で実行されていない場合
    • 自分自身を管理者権限で起動して終了

自分自身を管理者権限で起動する際に、利用局面によって、下記項目の情報を引数(それぞれ引数とするか、XMLファイルに情報を格納して XMLファイルを引数とする)で伝達すべきかをご検討ください。

  • 引数有りで起動されていた場合、同一起動引数を指定
  • 画面表示位置/サイズ(元画面と同一表示)
  • 管理者権限昇格する起因の処理(再起動で該当処理を自動実行)
  • 対象アプリで表示/入力/選択していた情報(アプリ状態の復元)

管理者権限で実行されているかは、下記コードで確認できます。

using System.Security.Principal;
private static bool IsRunAsAdmin()
{
  var principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
  return principal.IsInRole(WindowsBuiltInRole.Administrator);
}  

全体処理のサンプルコードは、Windows Forms - .NET 8 のみの記載とさせて頂きます。
本記事内の GetStartupKind、StartService、StopService、IsRunAsAdmin を利用するので、該当ソースは割愛します。

public partial class Form1 : Form
{
  private const string ServiceName = "MSSQL$Hoge";

  public Form1()
  {
    InitializeComponent();

    // 配置コントール
    btnExit.Click += btnExit_Click;
    btnRefresh.Click += btnRefresh_Click;
    btnStart.Click += btnStart_Click;
    btnStop.Click += btnStop_Click;

    // 初期値設定
    txtStartupKind.Text = GetStartupKind(ServiceName); // TODO:引数 → メンバ変数
    RefreshStatus();
  }

  // .NET Framework 時 object? の ? 不要
  private void btnExit_Click(object? sender, EventArgs e)
  {
    this.Close();
  }

  // .NET Framework 時 object? の ? 不要
  private void btnRefresh_Click(object? sender, EventArgs e)
  {
    RefreshStatus();
  }

  // .NET Framework 時 object? の ? 不要
  private void btnStart_Click(object? sender, EventArgs e)
  {
    if (IsRunAsAdmin())
    {
      // サービス開始 - TODO:引数 → メンバ変数
      StartService(ServiceName);
      // 表示更新
      RefreshStatus();
    }
    else
    {
      // 自分自身を管理者権限で起動して終了
      RunAsMyself();
    }
  }

  // .NET Framework 時 object? の ? 不要
  private void btnStop_Click(object? sender, EventArgs e)
  {
    if (IsRunAsAdmin())
    {
      // サービス停止 - TODO:引数 → メンバ変数
      StopService(ServiceName);
      // 表示更新
      RefreshStatus();
    }
    else
    {
      // 自分自身を管理者権限で起動して終了
      RunAsMyself();
    }
  }

  // サービスの状態 - 更新
  private void RefreshStatus()
  {
    // 初期化
    btnStart.Enabled = false;
    btnStop.Enabled = false;
    string sts = string.Empty;

    using (var sc = new ServiceController(ServiceName, "."))
    {
      if (sc != null)
      {
        if (sc.Status == ServiceControllerStatus.ContinuePending)
        {
          sts = "再開中";
        }
        else if (sc.Status == ServiceControllerStatus.Paused)
        {
          sts = "一時停止";
        }
        else if (sc.Status == ServiceControllerStatus.PausePending)
        {
          sts = "一時停止中";
        }
        else if (sc.Status == ServiceControllerStatus.Running)
        {
          sts = "実行中";
          btnStop.Enabled = true;
        }
        else if (sc.Status == ServiceControllerStatus.StartPending)
        {
          sts = "開始中";
        }
        else if (sc.Status == ServiceControllerStatus.StopPending)
        {
          sts = "停止中";
        }
        else if (sc.Status == ServiceControllerStatus.Stopped)
        {
          sts = "停止";
          btnStart.Enabled = true;
        }
      }
    }
    txtStatus.Text = sts;
  }

  // 自分自身を管理者権限で起動して終了
  private void RunAsMyself()
  {
    // .NET Framework 時は string? の ? 不要
    string? target = Process.GetCurrentProcess()?.MainModule?.FileName;
    if (target != null)
    {
      var proc = new ProcessStartInfo()
      {
        FileName = target,
        WorkingDirectory = Environment.CurrentDirectory,
        UseShellExecute = true,
        Verb = "RunAs"  // 管理者権限で実行
      };
      // 自分自身を管理者権限で起動
      Process.Start(proc);

     // 自分自身を終了
     this.Close();
    }
  }
}

出典

本記事は、2025/03/03 Qiita 投稿記事の転載です。

C#定石 - サービス制御 - SQL Server

Discussion