単純な Windows サービスを作成する

「え?これだけ・・・?」 Windows サービス (NT サービス) は簡単に作れる!

この資料では、サービスプログラムを作ります。

サービスは、表に出てこないでバックエンドで実行されるので、なんとなく難しそう、と思われがちですが、 全然そんなことはありません。Windows のサービスコントロールマネージャと通信するための、基本的なお作法にさえ従えばよいだけなのです。

この資料では非常に単純な Windows サービスを作って、インストールしてみます。 試してみれば、「なーんだ、結構簡単だな」 と思っていただけると思います。

ちなみに、この資料ではひたすらコードを書きます。 Windows サービスの仕組みや詳細について知りたい方は、 Windows サービスとは? をご覧ください。

では、さっそくコードを書いてみましょう!

Microsoft Windows サービスの作り方

Microsoft Windows サービス (以前は NT サービスと呼ばれていました) は非常に簡単に作成することができます。

最近は Visual Studio にも Windows サービス用のテンプレート (プロジェクト) が用意されていますので、 そういうテンプレートを利用したらとても簡単に出来ちゃいます。

この資料では Visual Studio のプロジェクトテンプレートではなく、地味なソースコード& makefile でゴリゴリ書きます。 プロジェクトを使うか、地味流で行くかは、ただの好みの問題なので、どっちでもいいのですが、この資料では後者の方法でいきます。

出来上がる Windows サービスは、有効な作業は何もしません (苦笑)。 ただ、デバッグトレースに、メッセージを出力するだけです。

これをみてもらえば、サービスが簡単に作れるということはお分りになるでしょう。

サンプル・プログラムの作成

ここでは解説ではなく、プログラムのソースコードを示します。

ダウンロード用の zip も用意しました。 [ソースコード・ダウンロード (demo-service.zip)]

ダウンロードして展開したら、すぐにビルドできると思いますので、下記の「ビルド」まで進んでください。

ヘッダファイル

以下のヘッダファイルを demo_service.h として保存します。

#pragma once

#include <windows.h>
#include <tchar.h>

DWORD WINAPI HandlerEx( 
	DWORD dwControl, 
	DWORD dwEventType, 
	PVOID pvEventData, 
	PVOID pvContext);
VOID WINAPI ServiceMain( DWORD dwArgc, PTSTR* pszArgv);
VOID DebugPrint (LPTSTR   szFormat, ... );

ソースファイル

以下のコードを demo_service.cpp として保存します。

//
// Demo Service
//

#include "demo_service.h"

#include <assert.h>
#include <stdarg.h>


///////////////////////////////////////////////////////////////////////////////


#define SERVICE_NAME (TEXT("Demo_Service"))
#define DEBUG_BUFF_SIZE (1024)


///////////////////////////////////////////////////////////////////////////////


SERVICE_TABLE_ENTRY ServiceTable[] = {
          { SERVICE_NAME, ServiceMain },
          { NULL, NULL }
};


///////////////////////////////////////////////////////////////////////////////


BOOL g_bRun = TRUE;
BOOL g_bService = TRUE;

SERVICE_STATUS_HANDLE g_hServiceStatus = NULL;


///////////////////////////////////////////////////////////////////////////////


VOID main() {

     BOOL bRet;
     
     bRet = StartServiceCtrlDispatcher (ServiceTable);

     assert(bRet);
     
}


////////////////////////////////////////////////////////////////////////////////


DWORD WINAPI HandlerEx ( 
	DWORD dwControl, 
	DWORD dwEventType, 
	LPVOID lpEventData, 
	LPVOID lpContext ) {

     SERVICE_STATUS ss;
     BOOL bRet;
     
     // Initialize Variables for Service Control
     ss.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
     ss.dwWin32ExitCode = NO_ERROR;
     ss.dwServiceSpecificExitCode = 0;
     ss.dwCheckPoint = 1;
     ss.dwWaitHint = 3000;
     ss.dwControlsAccepted = SERVICE_ACCEPT_STOP;

     switch(dwControl) {
     case SERVICE_CONTROL_STOP:

          DebugPrint (TEXT("SERVICE_CONTROL_STOP\n"));

          // Set STOP_PENDING status.
          ss.dwCurrentState = SERVICE_STOP_PENDING;
               
          bRet = SetServiceStatus (g_hServiceStatus, &ss);
          
          if (!bRet) {
               DebugPrint (TEXT("SetServiceStatus failed. %u\n"), GetLastError());
               break;
          }

          // SERVICE SPECIFIC STOPPING CODE HERE.
          // ...
          // ...
          
          g_bService = FALSE;
          Sleep (3 * 1000);


          // Set STOPPED status.
          ss.dwCurrentState = SERVICE_STOPPED;
          ss.dwCheckPoint = 0;
          ss.dwWaitHint = 0;
          
          bRet = SetServiceStatus (g_hServiceStatus, &ss);
                    
          if (!bRet) {
               DebugPrint (TEXT("SetServiceStatus failed. %u\n"), GetLastError());
               break;
          }

          break;

     case SERVICE_CONTROL_PAUSE:

          DebugPrint (TEXT("SERVICE_CONTROL_PAUSE\n"));

          // Set PAUSE_PENDING status.
          ss.dwCurrentState = SERVICE_PAUSE_PENDING;
                         
          bRet = SetServiceStatus (g_hServiceStatus, &ss);
                    
          if (!bRet) {
               DebugPrint (TEXT("SetServiceStatus failed. %u\n"), GetLastError());
               break;
          }

          // APPLICATION SPECIFIC PAUSE_PENDING CODE HERE.
          // ...
          // ...
          
          g_bRun = FALSE;

          // Set PAUSE_PENDING status.
          ss.dwCurrentState = SERVICE_PAUSED;
          ss.dwCheckPoint = 0;
          ss.dwWaitHint = 0;
          ss.dwControlsAccepted |= SERVICE_ACCEPT_PAUSE_CONTINUE;
                                   
          bRet = SetServiceStatus (g_hServiceStatus, &ss);
                              
          if (!bRet) {
               DebugPrint (TEXT("SetServiceStatus failed. %u\n"), GetLastError());
               break;
          }
          
          break;
          
     case SERVICE_CONTROL_CONTINUE:

          DebugPrint (TEXT("SERVICE_CONTROL_CONTINUE\n"));

          // Set PAUSE_PENDING status.
          ss.dwCurrentState = SERVICE_START_PENDING;
                                   
          bRet = SetServiceStatus (g_hServiceStatus, &ss);
                              
          if (!bRet) {
               DebugPrint (TEXT("SetServiceStatus failed. %u\n"), GetLastError());
               break;
          }
          
          // APPLICATION SPECIFIC START_PENDING CODE HERE.
          // ...
          // ...
                    
          g_bRun = TRUE;
          
          // Set RUNNING status.
          ss.dwCurrentState = SERVICE_RUNNING;
          ss.dwCheckPoint = 0;
          ss.dwWaitHint = 0;
          ss.dwControlsAccepted |= SERVICE_ACCEPT_PAUSE_CONTINUE;
                                             
          bRet = SetServiceStatus (g_hServiceStatus, &ss);
                                        
          if (!bRet) {
               DebugPrint (TEXT("SetServiceStatus failed. %u\n"), GetLastError());
               break;
          }
          
          break;
     default:

          return ERROR_CALL_NOT_IMPLEMENTED;

     }

     return NO_ERROR;
}


///////////////////////////////////////////////////////////////////////////////


VOID WINAPI ServiceMain(DWORD dwArgc, PTSTR* pszArgv) {

     BOOL bRet;
     SERVICE_STATUS ss;

     // Initialize Variables for Service Control
     ss.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
     ss.dwWin32ExitCode = NO_ERROR;
     ss.dwServiceSpecificExitCode = 0;
     ss.dwCheckPoint = 1;
     ss.dwWaitHint = 1000;
     ss.dwControlsAccepted = SERVICE_ACCEPT_STOP;

     // Register Service Control Handler
     
     g_hServiceStatus = 
          RegisterServiceCtrlHandlerEx (SERVICE_NAME, HandlerEx, NULL);

     if(0 == g_hServiceStatus) {
          DebugPrint (
               TEXT("RegisterServiceCtrlHandler failed. %u\n"), 
               GetLastError());
          return;
     }

     // Entering Starting Service.
     DebugPrint (TEXT("SERVICE_START_PENDING...\n"));
     
     ss.dwCurrentState = SERVICE_START_PENDING;
     
     bRet = SetServiceStatus (g_hServiceStatus, &ss);

     if (!bRet) {
          DebugPrint (TEXT("SetServiceStatus failed. %u\n"), GetLastError());
          return;
     }

     // APPLICATION SPECIFIC INITIALIZATION CODE
     // ...
     // ...
     
     // Finish Initializing.
     DebugPrint (TEXT("SERVICE_RUNNING.\n"));
     
     ss.dwCurrentState = SERVICE_RUNNING;
     ss.dwCheckPoint = 0;
     ss.dwWaitHint = 0;
     ss.dwControlsAccepted = 
          SERVICE_ACCEPT_PAUSE_CONTINUE |
          SERVICE_ACCEPT_STOP;
          
     bRet = SetServiceStatus (g_hServiceStatus, &ss);
          
     if (!bRet) {
          DebugPrint (TEXT("SetServiceStatus failed. %u\n"), GetLastError());
          return;
     }

     //
     // Service Main Code.
     //
     
     while(g_bService) {

          if(g_bRun) {
               DebugPrint (TEXT("%s is running.\n"), SERVICE_NAME);
          }

          Sleep(2 * 1000);
     }

     DebugPrint (TEXT("END OF ServiceMain\n"));
     
}


///////////////////////////////////////////////////////////////////////////////


VOID DebugPrint (LPTSTR   szFormat, ... ) {

     TCHAR    szBuffer[DEBUG_BUFF_SIZE + 1];
     INT     nWritten;
     va_list args;

     ::ZeroMemory(szBuffer, sizeof(szBuffer));

     // Format error message like printf()
     
     va_start( args, szFormat );

     nWritten = _vsntprintf( szBuffer, DEBUG_BUFF_SIZE, szFormat, args );

     va_end( args );

     // Output debug string

     ::OutputDebugString( szBuffer );

     
}

makefile

makefile は次の通りです。

TARGETNAME=demo_service
OUTDIR=.\chk

ALL : "$(OUTDIR)\$(TARGETNAME).exe"

"$(OUTDIR)" :
    @if not exist "$(OUTDIR)/$(NULL)" mkdir "$(OUTDIR)"

CPP_PROJ=\
	/MT\
	/W3\
	/nologo\
	/Fo"$(OUTDIR)\\"\
	/Fd"$(OUTDIR)\\"\
	/c\
	/DUNICODE\
	/D_UNICODE
		
LINK32=link.exe

LINK32_FLAGS=\
	kernel32.lib\
	advapi32.lib\
	user32.lib\
	/nologo\
	/subsystem:console\
	/pdb:"$(OUTDIR)\$(TARGETNAME).pdb"\
	/debug\
	/RELEASE\
	/machine:I386\
	/out:"$(OUTDIR)\$(TARGETNAME).exe"\
	
LINK32_OBJS= \
	"$(OUTDIR)\$(TARGETNAME).obj"

"$(OUTDIR)\$(TARGETNAME).exe" : "$(OUTDIR)" $(DEF_FILE) $(LINK32_OBJS)
    $(LINK32) $(LINK32_FLAGS) $(LINK32_OBJS)

.c{$(OUTDIR)}.obj::
   $(CPP) $(CPP_PROJ) $<

.cpp{$(OUTDIR)}.obj::
   $(CPP) $(CPP_PROJ) $<

ビルド

上記3ファイルから demo_service.exe をビルドします。

> nmake

いかがですか?いくつかワーニングが出たと思いますが、とりあえずそれは無視していただいて、 これでサブディレクトリ chk に demo_service.exe というプログラムが出来たはずです。

「おいおい、nmake なんて使ってないぞ」 という方は・・・、すみません、当サイトではそんな、古めかしいトラディショナルなやり方ばっかりやってまして・・・(苦笑) makefile の書き方 などに補足情報が書いてますのでご覧ください。

サービスのインストール

プログラムをサービスにするためには、サービスをレジストリへ登録しなければいけません。これがサービスのインストール作業になります。

Windows にはサービスを管理するためのコマンド sc コマンドが用意されていますので、これを使ってインストールします。 下記のように sc コマンドを実行するのですが、demo_service.exe のパスなどは、あなたの環境に合わせて書き換えてください。

> sc create DEMO_SERVICE binPath= C:\temp\demo1\chk\demo_service.exe 
displayname= "Demo Service"
[SC] CreateService SUCCESS

>

ちなみに、この sc コマンド、、、一体何なんでしょうね。

binPath= とか DisplayName= と書いたあとにスペースが無いと、オプションを認識しないんですね・・・(汗)
マイクロソフトさん、もっと真面目にパラメータ受け取り部分を書いてください。これ、ハッキリ言ってバクでしょ(笑)

と、少し脱線しましたが、上記のように [SC] CreateService SUCCESS と出れば成功です。

ここで、管理ツールのサービススナップインをみれば、ちゃんと Demo Service が登録されているはずです。

いかがですか?ちゃんと登録されていましたか?

サービスの開始方法

では、ここで作ったサービスを起動してみましょう!

と、その前に・・・ サービスの出力を見るための DebugView を起動してください。 DebugView はデバッグトレースを確認するためのツールです。入手方法などは DebugView のページ をご覧ください。

では、net コマンドでサービスを開始します。もちろん、コントロールパネルから開始しても問題ありません。

> net start DEMO_SERVICE
The Demo Service service is starting.
The Demo Service service was started successfully.


>

DebugView をみると、確かにデモ用のサービスが、デバッグ出力にメッセージを出力していることを確認できます。

サービスの停止

サービスのスタートと同様に、停止も net コマンドを使います。もちろん、コントロールパネルのスナップインを利用しても構いません。

> net stop DEMO_SERVICE

The Demo Service service was stopped successfully.

>

スタート、ストップを繰り返すと、デバッグトレースが出力されるので、コードのどの部分が実行されているかわかりやすいとおもいます。

サービスの削除

デモ用のサービスを登録しっぱなしでも特に問題はありませんが、 なんとなくごみを残すのは気持ち悪いと思います。サービスは次のコマンドで削除できます。

> sc delete DEMO_SERVICE
[SC] DeleteService SUCCESS

>

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説