8
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

WindowsサービスをC#で作る

Last updated at Posted at 2021-09-10

#はじめに
Visual Studio のテンプレートで基本部分はすぐ作れるものの、その後に色々あるのでメモ。

#プロジェクトの作成
ここでは Visual Studio 2019 で作ってみる。
まず、新しいプロジェクトの作成で、「Windows サービス(.NET Framework)」を作る。
.NET Frameworkのバージョンは多分どれでも良いが、とりあえずここでは4.5にする。少なくとも4.5なら、今のWindows10では初期状態でも動くっぽい。(.NET Frameworkの手動インストール不要)

#名前をつける
デフォルトでService1.csというファイルが作られる。
この名前がサービス名になってしまうので、まずはこれを変える。
ファイル名を変更すると、関連するところを修正するか聞かれるので、OKで自動修正する。

ファイル名の変更では変わらないところが1箇所(<サービス名>.cs[デザイン]タブのプロパティにある ServiceName)あるので、これも変更しておく。

#インストーラの追加
サービスとして登録できるようにするために、インストーラを追加する必要がある。
<サービス名>.cs[デザイン]タブ で右クリックし、メニューから「インストーラーの追加」を選択する。

ProjectInstaller.cs ファイルが追加される。
ProjectInstaller.cs[デザイン] タブに「ServiceInstaller1」の項目があるので、右クリックのプロパティにある「Description」にサービスの説明を書いておく。(services.mscで表示される説明文)

「DisplayName」にはサービス名(services.mscで表示されるサービス名)を書いておく。

また、StartTypeがデフォルトで「Manual」なので、「Automatic」にするとWindows起動時に自動起動するようになる。

#サービス実行アカウントの指定
ProjectInstaller.cs[デザイン] タブに「serviceProcessInstaller1」の項目があるので、右クリックのプロパティを開き、「Account」を変えることでサービス実行アカウントを選択する。
一般的には「LocalSystem」にすることが多い(イベントログの書き込み権限など、許可されていることが多いので)ようだが、必要な権限で決めるべき。
いわゆる管理者権限が必要ならば「LocalSystem」にする。
必要ない場合は「LocalService」で良いかもしれない。

#サービスのインストールについて
サービスはインストールすることでWindowsに登録され、services.mscに表示されるようになる。

インストール方法は、

  • installutil を使う
  • scを使う
  • InstallShieldやmsiの指定でインストールする
  • サービスのプログラムを実行することでインストールする

といった方法がある。
ここでは、サービスのプログラムを実行することでインストールする方法を説明する。

やり方は簡単で、ManagedInstallerClass.InstallHelper を呼び出せば良い。
具体的には以下のようになる。

using System.Configuration.Install;
using System.Reflection;

    .
    .
    .

try
{
    ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location });
}
catch (Exception e)
{
    Log.Write("install error: " + e.ToString());
}

これは毎回呼ぶわけにいかないので、コマンドライン引数で指定されたときに呼び出すようにする。
コマンドラインで実行しても、Console.Write ではなにも出力されないようなので注意。

static class Program
{
    static void Main(string[] args)
    {
    if (0 < args.Length) {
      string arg = args[0].ToLower().Trim();
      if (arg.Equals("/i")) {
        InstallService();
      }
      else if (arg.Equals("/u")) {
        UninstallService();
      }
      return;
    }

    // 以下に元々の Main
        ・
        ・
        ・
  }

    private static void InstallService()
    {
        try
        {
            ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location });
        }
        catch (Exception e)
        {
        Log.Write("install error: " + e.ToString());
        }
    }

    private static void UninstallService()
    {
        try
        {
            ManagedInstallerClass.InstallHelper(new string[] { "/u", Assembly.GetExecutingAssembly().Location });
        }
        catch (Exception e)
        {
        Log.Write("uninstall error: " + e.ToString());
        }
    }
}

これで、管理者権限で起動したコマンドプロンプトで、<サービス名>.exe /i と実行するとサービスが登録される。
アンインストールは /i の代わりに /u を使う。

#サービスの実行
<サービス名>.exe /i で登録しておけば、あとは通常のサービスと同様に実行できる。
service.msc を起動して「サービスの開始」か、コマンドプロンプトで net start "<サービス名>"で実行する。

net start で指定するサービス名は、ServiceInstaller1 の DisplayName で指定したものにする

#サービス本体の処理
サービス開始時に呼び出される OnStart と停止時に呼び出される OnStop を使う。OnStart, OnStopはテンプレートに含まれているので、自動的に作られる。

OnStartで別スレッドを開始するか、System.Timers.Timer を使って処理を実装する。
OnStopでスレッドやタイマーを終了する。

OnStart, OnStop に時間がかかる処理を入れる場合は、SetServiceStatus(https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-setservicestatus) を使って保留状態を追加すると良いが、ここでは省略。

#サービスから画面に通知メッセージを表示する
サービスでWindowやDialogの表示はそのままではできない。これはサービスを実行しているアカウントが、画面に表示する権限を持たないため。
このため、ログオン中のユーザ権限を使って CreateProcessAsUser し、通知用の別アプリを起動する。

そもそもサービスは誰もログオンしていなくても動いているので、OnSessionChangeを使って、ログオンしたら別アプリを起動、ログオフでアプリを終了し、プロセス間通信を使ってメッセージなどを表示させる。

#OnSessionChange
ユーザがログオン・ログオフ時に呼び出され、そこで session ID が得られるのでCreateProcessAsUserが実行できる。
OnSessionChange自体は普通にoverrideすれば良いが、CanHandleSessionChangeEvent = true; を実行しておく必要がある。
これはサービスのクラスのコンストラクタなどに書けばOK。

OnSessionChangeの例は以下の通り。

protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
  switch (changeDescription.Reason)
  {
    case SessionChangeReason.SessionLogon:
      startApplication(changeDescription.SessionId);
      break;
    case SessionChangeReason.SessionLogoff:
      stopApplication();
      break;
  }
}

void startApplication(int sesid) で通知メッセージ表示のアプリを起動する。
stopApplicationで終了。

#アプリ起動
アプリの起動は CreateProcessAsUserで行うが、その前にトークンを取得する必要がある。
トークンは WTSQueryUserToken で取得する。
この辺りのAPIはNativeMethodsにまとめるが、構造体が必要なのでちょっと面倒。

とりあえずまとめるとこんな感じ。

class NativeMethods
{
    [StructLayout(LayoutKind.Sequential)]
    public struct STARTUPINFO
    {
            public int cb;
            public String lpReserved;
            public String lpDesktop;
            public String lpTitle;
            public uint dwX;
            public uint dwY;
            public uint dwXSize;
            public uint dwYSize;
            public uint dwXCountChars;
            public uint dwYCountChars;
            public uint dwFillAttribute;
            public uint dwFlags;
            public short wShowWindow;
            public short cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_INFORMATION
    {
            public IntPtr hProcess;
            public IntPtr hThread;
            public uint dwProcessId;
            public uint dwThreadId;
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    {
            public int Length;
            public IntPtr lpSecurityDescriptor;
            public bool bInheritHandle;
    }

    [DllImport("wtsapi32.dll", SetLastError = true)]
    public static extern bool WTSQueryUserToken(UInt32 sessionId, out IntPtr Token);

    [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true,
               CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
    public extern static bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName,
        String lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes,
        ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, int dwCreationFlags,
        IntPtr lpEnvironment, String lpCurrentDirectory, ref STARTUPINFO lpStartupInfo,
        out PROCESS_INFORMATION lpProcessInformation);
    public static IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;
    public static Int32 WTS_CURRENT_SESSION = -1;
}

この定義をした上で、startApplicationは以下のようになる。

    private void startApplication(int sesid)
    {
            IntPtr token = IntPtr.Zero;
            bool res = NativeMethods.WTSQueryUserToken((uint)sesid, out token);
            if (!res)
            {
                Log.Write("WTSQueryUserToken failed: " + Marshal.GetLastWin32Error());
                return;
            }
            string path = "C:\\temp\\servicemessage.exe"; // メッセージ表示の実行ファイル
            string dir = "C:\\temp"; // 実行時のディレクトリ
            var si = new NativeMethods.STARTUPINFO();
            si.lpDesktop = "winsta0\\default";
            si.cb = Marshal.SizeOf(si);
            var pi = new NativeMethods.PROCESS_INFORMATION();
            var sa = new NativeMethods.SECURITY_ATTRIBUTES();
            sa.bInheritHandle = false;
            sa.Length = Marshal.SizeOf(sa);
            sa.lpSecurityDescriptor = IntPtr.Zero;
            res = NativeMethods.CreateProcessAsUser(token, path, string.Empty, ref sa, ref sa,
                false, 0, IntPtr.Zero, dir, ref si, out pi);
            if (!res)
            {
                Log.Write("CreateProcessAsUser failed: " + Marshal.GetLastWin32Error());
            }
        }

ちゃんとやるなら実行ファイルのパスは Assembly.GetExecutingAssembly().Location とかから作るのが良さそう。

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?