最初にC#に触れてから1年近くが経とうとしているが、3ヶ月ほど前から本格的なシステムの開発を始めた。そういうわけでいろいろなところでつまづきまくっている。今回はWindowsサービスアプリケーションの開発。何とか道が見えたところで、記憶にとどめておくべきことをメモしておきたい。
クリティカルな部分をサービスに切り出して、必要なときだけGUIを立ち上げればよいような構造にしている。ゼロからの開発になるので、サービス・GUI・クラスライブラリを全部1つのソリューションの中で管理している。
サービスアプリ自体の開発はいったんやり方を見つければ簡単なんだけど、そこに行き着くまでが大変だった。ウェブ上にも古いやり方しか載っていない。installutil.exeとかコマンドラインツールを使う方法は古い。普通のアプリと同じ方法でセットアッププロジェクトを作ったあと、カスタム動作を設定するだけだ。このカスタム動作の設定の仕方は次のとおり。
1. ソリューションエクスプローラから、セットアッププロジェクトを右クリック->表示->カスタム動作
2. 開いた画面で、カスタム動作アイコンを右クリック->カスタム動作の追加
3. 開いたダイアログで、アプリケーションフォルダをダブルクリック。
5. 2の画面の各アクションにプライマリ出力が追加される。各アクション(イベント)のコールバックはProjectInstaller.csに自動的に実装される。
私は、これにインストール時に起動、アンインストール時に停止するアクションを加えた。ProjectInstallerのデザイナのイベントプロパティから、AfterInstall, BeforeUninstallイベントハンドラを追加する。ソース全体は以下のようになった。
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Configuration.Install; using System.Linq; using System.ServiceProcess; using System.Windows.Forms; namespace SignalService { [RunInstaller(true)] public partial class ProjectInstaller : Installer { public ProjectInstaller() { InitializeComponent(); } public override void Install(IDictionary stateSaver) { base.Install(stateSaver); } public override void Commit(IDictionary savedState) { base.Commit(savedState); } public override void Rollback(IDictionary savedState) { base.Rollback(savedState); } public override void Uninstall(IDictionary savedState) { base.Uninstall(savedState); } private void ProjectInstaller_AfterInstall(object sender, InstallEventArgs e) { ServiceController controller = new ServiceController(serviceInstaller1.ServiceName); controller.Start(); } private void ProjectInstaller_BeforeUninstall(object sender, InstallEventArgs e) { ServiceController controller = new ServiceController(serviceInstaller1.ServiceName); try { controller.Stop(); } catch (Exception) { } } } }
過去にうちの先輩がC++で開発した、リモートデバッグ機能付きのロギングDLLを使おうとしたのだが、実はサービスアプリケーションで使うのは初めてだった。DllImportして使うわけだが、サービスが起動しているときに、GUIからもこのDLLを呼ぶと、System.DllNotFoundExceptionが発生する。サービスでなければ2つのアプリケーションから同時に使うことは問題なかったので、多分ユーザ権限の問題なのだろう。サービスはLocalSystemで動作させていた。このDLL特有の問題なのかもしれない。
また、ログファイルの出力先をカレントディレクトリのlogフォルダ(“.log”)としていたのだが、なぜかファイルが書かれない。絶対パスを指定するとちゃんと書かれるので、Program Filesに書くには特別な権限が必要なんだろうかと想像したりもしたが、デバッガをアタッチしてみると、実は単にカレントディレクトリが C:WINDOWSsystem32 になっていることが分かった。そこには普通にログが書き出されていた。以下のようにカレントディレクトリを移動することで解決した。
private void MoveCurrentDirectory() { Assembly exe = Assembly.GetEntryAssembly(); FileInfo exeFileInfo = new FileInfo(exe.Location); Directory.SetCurrentDirectory(exeFileInfo.DirectoryName); }
結局このDLLはサービスで使うと他で使えないようだったので、サービス用のロガーはC#で書いた。たいした機能はないが、キューイングして別スレッドで遅延書き込みみたいなことを実装した。リモートデバッグはとりあえず、Debug.Printで我慢する。