#はじめに
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 とかから作るのが良さそう。