「サービスの開始種類をコードから変更したいのに、ServiceController にはプロパティがない。P/Invoke で ChangeServiceConfig を呼ぶとハンドル型でつまずく。閉じ方も不安」──.NET Framework 4.8 の現場で誰もが一度はぶつかる壁です。本稿はその悩みを一気に解消します。安全なハンドル管理、正しい P/Invoke 宣言、アクセス権の落とし穴、遅延自動開始(ChangeServiceConfig2)まで、実運用に耐えるコードと共に徹底解説します。
背景と課題の整理
.NET Framework 4.8 の ServiceController クラスは、サービス開始種類(自動/自動(遅延)/手動/無効)の変更 API を公開していません。そのため Win32 API の ChangeServiceConfig(および遅延自動開始には ChangeServiceConfig2)を P/Invoke で呼ぶのが定石です。ところが次の2点でよくハマります。
- ハンドル型の不一致:
ServiceController.ServiceHandleはSafeHandleですが、P/Invoke をIntPtr受けで宣言していると「SafeHandleをIntPtrに変換できない」コンパイル エラーになります。 - ハンドルのクローズ規約:
ServiceControllerが返すハンドルを自分でCloseServiceHandleすべきか、ServiceHandle.Close()/ServiceController.Close()を呼ぶべきか分からない。
結論(まずは最短の実装)
以下の2点を守れば、.NET 4.8 でも安全・簡潔に開始種類を変更できます。
- P/Invoke の第一引数は
IntPtrで宣言し、呼び出し側ではsc.ServiceHandle.DangerousGetHandle()を渡す。 - 自前で
CloseServiceHandleは呼ばない。ServiceControllerをUsingで囲み、Disposeに任せる。
Imports System.Runtime.InteropServices
Imports System.ComponentModel
Imports System.ServiceProcess
Module ServiceStartType
<DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Function ChangeServiceConfig(
hService As IntPtr,
dwServiceType As UInteger,
dwStartType As UInteger,
dwErrorControl As UInteger,
lpBinaryPathName As String,
lpLoadOrderGroup As String,
lpdwTagId As IntPtr,
lpDependencies As String,
lpServiceStartName As String,
lpPassword As String,
lpDisplayName As String
) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
Public Sub SetStartupType(serviceName As String, startType As UInteger)
Const SERVICE_NO_CHANGE As UInteger = &HFFFFFFFFUI
Using sc As New ServiceController(serviceName)
Dim h As IntPtr = sc.ServiceHandle.DangerousGetHandle()
Dim ok = ChangeServiceConfig(h, SERVICE_NO_CHANGE, startType, SERVICE_NO_CHANGE,
Nothing, Nothing, IntPtr.Zero, Nothing, Nothing, Nothing, Nothing)
If Not ok Then Throw New Win32Exception(Marshal.GetLastWin32Error())
End Using ' ← ここでハンドル自動解放
End Sub
End Module
ポイント:DangerousGetHandle() は “生の” IntPtr を取り出す危険な API ですが、ServiceController を Using で囲めばライフタイムが担保されます。二重解放(double free)を防ぐため、返ってきたハンドルを自分で閉じないでください。
開始種類の値一覧(Win32 定数)
| 開始種類 | 定数 | 値 | 備考 |
|---|---|---|---|
| 変更しない | SERVICE_NO_CHANGE | 0xFFFFFFFF | 他パラメータのみ変更 |
| 自動 | SERVICE_AUTO_START | 0x00000002 | 「遅延」は別 API(後述) |
| 手動 | SERVICE_DEMAND_START | 0x00000003 | オンデマンド |
| 無効 | SERVICE_DISABLED | 0x00000004 | 起動不可 |
Public Module ServiceStartTypeConst
Public Const SERVICE_NO_CHANGE As UInteger = &HFFFFFFFFUI
Public Const SERVICE_AUTO_START As UInteger = 2UI
Public Const SERVICE_DEMAND_START As UInteger = 3UI
Public Const SERVICE_DISABLED As UInteger = 4UI
End Module
ハンドル管理の正解/やってよいこと・ダメなこと
| 状況 | やること | やってはいけないこと |
|---|---|---|
ServiceController.ServiceHandle を使う | Using sc ... End Using で囲み、sc.ServiceHandle.DangerousGetHandle() を IntPtr として渡す | CloseServiceHandle を自分で呼ぶ/ServiceHandle.Close() を勝手に呼ぶ |
P/Invoke で自前に OpenService した | 必ず CloseServiceHandle で閉じる(Try...Finally または SafeHandle で管理) | 閉じ忘れ(ハンドルリーク) |
| 64/32bit 混在 | CharSet.Unicode と SetLastError:=True を宣言に付ける | ANSI 宣言/エラーコード未取得 |
アクセス権の落とし穴:SERVICE_CHANGE_CONFIG が必要
ChangeServiceConfig/ChangeServiceConfig2 を呼ぶには、対象サービスを SERVICE_CHANGE_CONFIG 権限で開いている必要があります。多くの環境では管理者(UAC 昇格済み)でないと ERROR_ACCESS_DENIED (5) になります。
通常は ServiceController が十分な権限でハンドルを開いてくれますが、ドメイン環境やリモート接続では不足する場合があります。その場合だけ「自前で開く→閉じる」のパスを用意しておくと堅牢です。
Imports System.Runtime.InteropServices
Imports System.ComponentModel
Module NativeOpen
Private Const SC_MANAGER_CONNECT As UInteger = &H1
Private Const SERVICE_CHANGE_CONFIG As UInteger = &H2
<DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Function OpenSCManager(machineName As String, databaseName As String, dwDesiredAccess As UInteger) As IntPtr
End Function
<DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Function OpenService(hSCManager As IntPtr, lpServiceName As String, dwDesiredAccess As UInteger) As IntPtr
End Function
<DllImport("advapi32.dll", SetLastError:=True)>
Private Function CloseServiceHandle(hSCObject As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
Public Sub SetStartupTypeFallback(serviceName As String, startType As UInteger, Optional machine As String = Nothing)
Const SERVICE_NO_CHANGE As UInteger = &HFFFFFFFFUI
Dim scm As IntPtr = IntPtr.Zero, svc As IntPtr = IntPtr.Zero
Try
scm = OpenSCManager(machine, Nothing, SC_MANAGER_CONNECT)
If scm = IntPtr.Zero Then Throw New Win32Exception(Marshal.GetLastWin32Error())
svc = OpenService(scm, serviceName, SERVICE_CHANGE_CONFIG)
If svc = IntPtr.Zero Then Throw New Win32Exception(Marshal.GetLastWin32Error())
' Change
If Not ChangeServiceConfig(svc, SERVICE_NO_CHANGE, startType, SERVICE_NO_CHANGE,
Nothing, Nothing, IntPtr.Zero, Nothing, Nothing, Nothing, Nothing) Then
Throw New Win32Exception(Marshal.GetLastWin32Error())
End If
Finally
If svc <> IntPtr.Zero Then CloseServiceHandle(svc) : svc = IntPtr.Zero
If scm <> IntPtr.Zero Then CloseServiceHandle(scm) : scm = IntPtr.Zero
End Try
End Sub
End Module
注意:上の関数は 自前で開いたハンドル なので、CloseServiceHandle を必ず呼びます。ServiceController から取得したハンドルを渡しているわけではありません。
「自動(遅延開始)」を設定する:ChangeServiceConfig2
GUI の「自動(遅延開始)」に相当する設定は ChangeServiceConfig2 の SERVICE_CONFIG_DELAYED_AUTO_START_INFO で行います。開始種類を SERVICE_AUTO_START にした上で、遅延フラグを True に設定します。
Imports System.Runtime.InteropServices
Imports System.ComponentModel
Imports System.ServiceProcess
Module DelayedAutoStart
Private Const SERVICE_CONFIG_DELAYED_AUTO_START_INFO As UInteger = 3UI
<StructLayout(LayoutKind.Sequential)>
Private Structure SERVICE_DELAYED_AUTO_START_INFO
<MarshalAs(UnmanagedType.Bool)>
Public fDelayedAutostart As Boolean
End Structure
<DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Function ChangeServiceConfig2(
hService As IntPtr,
dwInfoLevel As UInteger,
ByRef lpInfo As SERVICE_DELAYED_AUTO_START_INFO
) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
Public Sub SetDelayedAutoStart(serviceName As String, enable As Boolean)
Using sc As New ServiceController(serviceName)
Dim h = sc.ServiceHandle.DangerousGetHandle()
' 1) 開始種類を自動へ(必要なら)
Const SERVICE_NO_CHANGE As UInteger = &HFFFFFFFFUI
Const SERVICE_AUTO_START As UInteger = 2UI
If Not ChangeServiceConfig(h, SERVICE_NO_CHANGE, SERVICE_AUTO_START, SERVICE_NO_CHANGE,
Nothing, Nothing, IntPtr.Zero, Nothing, Nothing, Nothing, Nothing) Then
Throw New Win32Exception(Marshal.GetLastWin32Error())
End If
' 2) 遅延自動のフラグ設定
Dim info As New SERVICE_DELAYED_AUTO_START_INFO With {.fDelayedAutostart = enable}
If Not ChangeServiceConfig2(h, SERVICE_CONFIG_DELAYED_AUTO_START_INFO, info) Then
Throw New Win32Exception(Marshal.GetLastWin32Error())
End If
End Using
End Sub
End Module
現在の開始種類を読み取る(検証用)
テストやログのために、QueryServiceConfig で現在の開始種類を取得する関数も用意しておくと便利です。
Imports System.Runtime.InteropServices
Imports System.ComponentModel
Imports System.ServiceProcess
Module QueryConfig
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
Private Structure QUERY_SERVICE_CONFIG
Public dwServiceType As UInteger
Public dwStartType As UInteger
Public dwErrorControl As UInteger
Public lpBinaryPathName As IntPtr
Public lpLoadOrderGroup As IntPtr
Public dwTagId As UInteger
Public lpDependencies As IntPtr
Public lpServiceStartName As IntPtr
Public lpDisplayName As IntPtr
End Structure
<DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Function QueryServiceConfig(hService As IntPtr, lpServiceConfig As IntPtr,
cbBufSize As Integer, ByRef pcbBytesNeeded As Integer) As Boolean
End Function
Public Function GetStartType(serviceName As String) As UInteger
Using sc As New ServiceController(serviceName)
Dim h = sc.ServiceHandle.DangerousGetHandle()
Dim needed As Integer = 0
QueryServiceConfig(h, IntPtr.Zero, 0, needed)
Dim last = Marshal.GetLastWin32Error()
Dim buf = Marshal.AllocHGlobal(needed)
Try
If Not QueryServiceConfig(h, buf, needed, needed) Then
Throw New Win32Exception(Marshal.GetLastWin32Error())
End If
Dim cfg = Marshal.PtrToStructure(Of QUERY_SERVICE_CONFIG)(buf)
Return cfg.dwStartType
Finally
Marshal.FreeHGlobal(buf)
End Try
End Using
End Function
End Module
本番で使えるヘルパー API(まとめ)
上記をひとつのモジュールにまとめ、用途別の関数を公開しておくと再利用しやすくなります。
Imports System.ComponentModel
Imports System.ServiceProcess
Public Module ServiceConfigurator
Public Enum StartupType As UInteger
NoChange = &HFFFFFFFFUI
AutoStart = 2UI
DemandStart = 3UI
Disabled = 4UI
End Enum
Public Sub SetStartupType(serviceName As String, type As StartupType)
NativeGuard.AssertElevated() ' 任意:昇格チェック(実装例は後述)
ServiceStartType.SetStartupType(serviceName, CUInt(type))
End Sub
Public Sub EnableDelayedAutoStart(serviceName As String, enable As Boolean)
NativeGuard.AssertElevated()
DelayedAutoStart.SetDelayedAutoStart(serviceName, enable)
End Sub
Public Function ReadStartupType(serviceName As String) As StartupType
Return CType(QueryConfig.GetStartType(serviceName), StartupType)
End Function
End Module
昇格(UAC)と権限のチェック
開始種類の変更には通常ローカル管理者のトークンが必要です。テスト時に権限不足で悩まないよう、プロセスが昇格しているかを先に検査すると UX が向上します。
Imports System.Security.Principal
Public Module NativeGuard
Public Sub AssertElevated()
Dim id = WindowsIdentity.GetCurrent()
Dim principal = New WindowsPrincipal(id)
If Not principal.IsInRole(WindowsBuiltInRole.Administrator) Then
Throw New UnauthorizedAccessException("管理者として実行してください(UAC昇格が必要)。")
End If
End Sub
End Module
典型エラーと対処
| Win32 エラー | 状況 | 対処 |
|---|---|---|
5 (ERROR_ACCESS_DENIED) | 権限不足/UAC 未昇格/サービスの ACL が厳しい | 管理者で実行、または OpenService(..., SERVICE_CHANGE_CONFIG) のフォールバックを使用 |
87 (ERROR_INVALID_PARAMETER) | 開始種類の値が不正、または組み合わせが矛盾 | 定数を確認。遅延開始は ChangeServiceConfig2 を併用 |
1060 (ERROR_SERVICE_DOES_NOT_EXIST) | サービス名の誤り/ターゲット環境に存在しない | ServiceController.GetServices() で列挙して確認 |
6 (ERROR_INVALID_HANDLE) | 無効ハンドルを渡した | ハンドルのライフタイムを Using で保証。自前で閉じない |
動作確認のミニスクリプト
テストは専用の検証用サービスで行うことを推奨します(OS 既定の重要サービスは避ける)。以下は一連の流れの例です。
Sub Demo()
Dim name = "YourTestService"
Console.WriteLine("Before: " & ServiceConfigurator.ReadStartupType(name))
ServiceConfigurator.SetStartupType(name, ServiceConfigurator.StartupType.DemandStart)
Console.WriteLine("Set to Manual.")
Console.WriteLine("Now: " & ServiceConfigurator.ReadStartupType(name))
ServiceConfigurator.SetStartupType(name, ServiceConfigurator.StartupType.AutoStart)
ServiceConfigurator.EnableDelayedAutoStart(name, True)
Console.WriteLine("Set to Automatic (Delayed).")
Console.WriteLine("Now: " & ServiceConfigurator.ReadStartupType(name))
End Sub
ServiceController リフレッシュのコツ
ServiceController は状態をキャッシュします。設定変更直後に UI などへ反映する場合は、同一インスタンスで sc.Refresh() を呼ぶか、新しい ServiceController を作り直してください。特に「サービス管理コンソール(services.msc)」を開いたままの検証では表示が遅れて見えることがあります。
安全性と運用面のベストプラクティス
- トランザクション性:
ChangeServiceConfigは呼び出し単位で変更され、途中失敗時は既存設定が残ります。複数項目を変えるときは失敗時のロールバック方針を決めておきましょう。 - 依存関係:依存サービスがある場合、無効化は可能ですが運用影響が大きいです。事前に依存関係を列挙し(
QueryServiceConfigのlpDependencies)、変更計画に反映します。 - 監査:だれが・いつ・どのサービスを・何に変更したかをアプリ側でログに残すと追跡が容易です。
- リモート変更:
OpenSCManager(machine, ...)で\\SERVERを指定すればリモートも可能。ファイアウォールと資格情報に注意。 - ビルド構成:AnyCPU/x64 いずれでも動作しますが、宣言は
CharSet.Unicodeに統一し、ANSI にならないよう注意。
PowerShell との使い分け
運用ツールとしては PowerShell の Set-Service -StartupType が簡便です。アプリ内で完結させたい/GUI から切り替えたい/監査や権限昇格を自前で制御したい、といった要件では本稿の .NET 実装が有効です。
FAQ(よくある質問)
Q. ChangeServiceConfig の第一引数を SafeHandle で宣言してはダメ?
A. 型定義上は可能ですが、ServiceController の戻り値は内部型(派生 SafeHandle)であり、P/Invoke 側の宣言と完全一致させるのが面倒です。最も実務的なのは IntPtr 受け + DangerousGetHandle() の組み合わせです。
Q. CloseServiceHandle を自分で呼ぶべきケースは?
A. 自前で OpenSCManager/OpenService したときのみです。ServiceController から入手したハンドルは ServiceController.Dispose() が閉じます。
Q. 実行中のサービスを「無効」にしても大丈夫?
A. 実行中でも設定変更自体は可能です。次回起動がブロックされるだけで、直ちに停止はされません。運用上の影響を理解したうえで行いましょう。
Q. 設定変更が反映されない/GUI と食い違う
A. キャッシュの可能性があります。sc.Refresh()、新しいインスタンスの作成、あるいはサービス管理コンソールを開き直してください。
落とし穴チェックリスト
| チェック項目 | 説明 |
|---|---|
| UAC 昇格済みか | 管理者トークンで実行しているかを確認(サンプルの AssertElevated() を活用) |
| 開始種類の定数値 | Auto=2, Manual=3, Disabled=4, NoChange=0xFFFFFFFF |
| 遅延自動の設定順序 | まず自動開始にし、その後 ChangeServiceConfig2 で遅延フラグを立てる |
| ハンドルの閉じ方 | ServiceController の場合は 閉じない/自前で開いた場合は 必ず閉じる |
| エラー番号の記録 | Marshal.GetLastWin32Error() を例外に乗せてログへ |
(参考)C# での最小実装
読者の混在環境向けに、C# 版の最小コードも掲載しておきます。基本は VB と同じです。
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.ServiceProcess;
static class ServiceTools
{
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool ChangeServiceConfig(
IntPtr hService, uint type, uint startType, uint errorControl,
string bin, string group, IntPtr tagId, string deps, string startName, string pwd, string display);
const uint SERVICE_NO_CHANGE = 0xFFFFFFFF;
public const uint SERVICE_AUTO_START = 2;
public const uint SERVICE_DEMAND_START = 3;
public const uint SERVICE_DISABLED = 4;
public static void SetStartupType(string name, uint startType)
{
using var sc = new ServiceController(name);
var h = sc.ServiceHandle.DangerousGetHandle();
if (!ChangeServiceConfig(h, SERVICE_NO_CHANGE, startType, SERVICE_NO_CHANGE,
null, null, IntPtr.Zero, null, null, null, null))
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
まとめ
- P/Invoke の第一引数は
IntPtr受け、呼び出しはDangerousGetHandle()で OK。 - ハンドル解放は
Using/Disposeに任せる。ServiceController由来のハンドルにCloseServiceHandleを掛けない。 - 権限不足に配慮し、必要に応じて
OpenSCManager/OpenServiceでSERVICE_CHANGE_CONFIGを確保。 - 「自動(遅延)」は
ChangeServiceConfig2を併用してフラグを設定。 - エラー処理・ログ・リフレッシュを整備すれば、.NET Framework 4.8 でも実運用で安全にサービスの開始種類を制御できます。
付録:よく使うコード断片(コピペ可)
定数・宣言ひとまとめ
Public Module ServiceNative
Public Const SERVICE_NO_CHANGE As UInteger = &HFFFFFFFFUI
Public Const SERVICE_AUTO_START As UInteger = 2UI
Public Const SERVICE_DEMAND_START As UInteger = 3UI
Public Const SERVICE_DISABLED As UInteger = 4UI
Public Const SERVICE_CONFIG_DELAYED_AUTO_START_INFO As UInteger = 3UI
<DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Public Function ChangeServiceConfig(
hService As IntPtr, dwServiceType As UInteger, dwStartType As UInteger, dwErrorControl As UInteger,
lpBinaryPathName As String, lpLoadOrderGroup As String, lpdwTagId As IntPtr,
lpDependencies As String, lpServiceStartName As String, lpPassword As String, lpDisplayName As String
) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
<StructLayout(LayoutKind.Sequential)>
Public Structure SERVICE_DELAYED_AUTO_START_INFO
<MarshalAs(UnmanagedType.Bool)> Public fDelayedAutostart As Boolean
End Structure
<DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Public Function ChangeServiceConfig2(
hService As IntPtr, dwInfoLevel As UInteger, ByRef lpInfo As SERVICE_DELAYED_AUTO_START_INFO
) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
End Module
ユースケース別ワンライナー
' Manual にする
ServiceStartType.SetStartupType("YourService", SERVICE_DEMAND_START)
' Disabled にする
ServiceStartType.SetStartupType("YourService", SERVICE_DISABLED)
' Automatic(Delayed) にする
DelayedAutoStart.SetDelayedAutoStart("YourService", True)
安全な例外化テンプレート
Private Sub ThrowOnLastErrorIfFalse(ok As Boolean, Optional action As String = Nothing)
If Not ok Then
Dim code = Marshal.GetLastWin32Error()
Dim msg = If(String.IsNullOrEmpty(action), "", $"({action}) ")
Throw New Win32Exception(code, $"{msg}Win32Error={code}")
End If
End Sub
ServiceController のキャッシュ更新
Using sc As New ServiceController("YourService")
' ... 変更処理 ...
sc.Refresh() ' ← これで最新状態を再取得
End Using
以上で、ハンドル管理と ChangeServiceConfig 呼び出しに関する“落とし穴”はすべて回避できます。現場のユースケースに合わせて、ここで示した最短パス(ServiceController のハンドルをそのまま使う)と、権限不足時のフォールバックの二段構えで実装しておくのがプロダクション品質です。

コメント