Windowsで、電源が投入されると同時に(すなわちWindowsが起動されると同時に)プログラムを起動するには、一般的に下記の三つの方法がある。
-
スタートアップにショートカットを登録する。
-
レジストリに登録する。
-
サービスに登録する。
上記の内、1と2は、誰かがログインしないとプロセスが起動されない。しかし、ごく普通のショートカットを配置したり、あるいは起動時のパス名をレジストリに登録するだけで自動的に起動させることが可能となる。
サービスに登録すれば、誰かがログインしなくてもPCが起動すると同時にプロセスを起動することが可能となる。しかし、サービスに登録するには、プログラムをサービスとして起動されるための特殊な作りにしてやる必要がある。
でもって、サービスとして起動されるためのインタフェースはC言語のものである。すなわち、サービスとして起動されるプログラムはCもしくはC++(あるいはC言語のインタフェースを提供することができるその他の言語)である必要がある。
だから、一般的にはJavaで作ったプログラムをサービスに登録することはできない。
上の方にも書いたが、サービスとして起動される部分はCで記述する必要がある。
起動の順番とモジュール構成の概略を示すと下記のようになる。

PCに電源が投入されると同時に、Windowsが動きだし、じきにWindowsの一部であるサービス制御マネージャが動作を開始する。
サービス制御マネージャはレジストリへの登録内容を参照して順番にサービスとして登録されたプログラムを起動する。その中に、この章で解説する起動用モジュールが含まれていれば、そのモジュールが起動されることになる。
起動用モジュールは、起動されるとすぐにJavaで作成された本体のプログラムを起動する。その後、起動用モジュールは、サービス制御マネージャから送られてくる電源断のイベントを受け取るとJavaで作られた本体のプロセスを終了させ自分自身も処理を終了する。
上図に示されているよう、起動用モジュールは本体のプロセスを起動したらすぐに処理を終わってしまうのではなく、その後もずっとプロセスとしては残り続けて、本体のプログラムを終了させる処理まで面倒を見てやる必要がある。
これは、サービスとして登録されるプログラムとしてしなければならない処理であり、好むと好まざるとに関わらず、従わなければならない仕様である。
下記に、起動用モジュールのプログラムを示す。
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <windows.h>
#include <io.h>
#include <fcntl.h>
#include <errno.h>
HANDLE glb_Semaphore; // セマフォ
SERVICE_STATUS_HANDLE glb_ServiceStatusHandle;
// Javaで作られた本体のプログラムを終了させる処理を行う
void KillServer( int fdpipe )
{
// 子プロセスに対して終了すべきことを通知する
write( fdpipe, "stop", 4 );
write( fdpipe, "\n", 1 );
}
// Javaで作られた本体のプログラムを起動する
int StartServer()
{
char Command[十分な長さ];
int hStdInPipe[2];
int i, j, k;
// CLASSPATH環境変数を設定
strcpy( Command, "CLASSPATH=起動する.jarファイルのパス名" );
_putenv( Command );
// サーバ起動用コマンドを生成
strcpy( Command, "起動するためのコマンド" );
// パイプを作成する。
if( _pipe( hStdInPipe, 512, O_TEXT | O_NOINHERIT ) )
return -1;
// 読み込みパイプを標準入力ハンドルに結合する。
if( _dup2( hStdInPipe[0], 0 ) ) return -1;
// 元の読み込みパイプを閉じる。
close( hStdInPipe[0] );
// サーバを起動
_spawnlp( _P_NOWAIT, "javaw.exe", "javaw", Command, NULL );
return hStdInPipe[1];
}
// サービス制御マネージャから呼ばれる。
// PCで発生したイベントが通知される。
void WINAPI Handler( DWORD fdwControl )
{
SERVICE_STATUS stat;
stat.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
stat.dwCurrentState = SERVICE_RUNNING;
stat.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
stat.dwWin32ExitCode = NO_ERROR;
stat.dwServiceSpecificExitCode = NO_ERROR;
stat.dwCheckPoint = 0;
stat.dwWaitHint = 0;
switch ( fdwControl ) {
case SERVICE_CONTROL_STOP:
case SERVICE_CONTROL_SHUTDOWN:
// ServiceMain関数で待ち合わせているスレッド
// ((A)で止まっている)の動作を再開させる。
// これにより、Javaで作られた本体のプログラムを終了させる。
stat.dwCurrentState = SERVICE_STOP_PENDING;
ReleaseSemaphore( glb_Semaphore, 1, NULL );
break;
}
SetServiceStatus( glb_ServiceStatusHandle, &stat);
}
// サービス制御マネージャから呼び出される。
// サービスを開始し、サービスが終了するまで
// (すなわちPCの電源が落とされるまで)の処理を行う。
void WINAPI ServiceMain( DWORD argc, LPTSTR* argv )
{
SERVICE_STATUS stat;
int fdWritePipe;
// サービスの要求ハンドラを設定
glb_ServiceStatusHandle =
RegisterServiceCtrlHandler( "MyService", Handler );
// 待機用のセマフォを構築
glb_Semaphore = CreateSemaphore( NULL, 0, 1, NULL );
// 状態を通知
stat.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
stat.dwCurrentState = SERVICE_START_PENDING;
stat.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
stat.dwWin32ExitCode = NO_ERROR;
stat.dwServiceSpecificExitCode = NO_ERROR;
stat.dwCheckPoint = 0;
stat.dwWaitHint = 1000;
SetServiceStatus( glb_ServiceStatusHandle, &stat);
// サーバーを起動する
fdWritePipe = StartServer();
// 状態を通知
stat.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus( glb_ServiceStatusHandle, &stat);
// サーバの終了を待ち合わせる(A)
WaitForSingleObject( glb_Semaphore, INFINITE );
// セマフォを破棄
CloseHandle( glb_Semaphore );
// サーバプロセスを終了させる
KillServer( fdWritePipe );
// 終了を通知する
stat.dwWaitHint = 0;
stat.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus( glb_ServiceStatusHandle, &stat);
}
// プログラムの起動時に呼び出される
int main( int argc, char *argv[] )
{
SERVICE_TABLE_ENTRY ste[2];
ste[0].lpServiceName = "MyService"; // サービス名
ste[0].lpServiceProc = ServiceMain; // 呼び出し先関数
ste[1].lpServiceName = NULL;
ste[1].lpServiceProc = NULL;
StartServiceCtrlDispatcher( ste );
return 0;
}
|
上記はmain・ServiceMain・Handler・StartServer・KillServerの5つの関数から構成されている。
main関数
いわずと知れたmain関数である。
main関数では、SERVICE_TABLE_ENTRY構造体に値を設定して、StartServiceCtrlDispatcher関数を呼び出しているのみである。
MSDNには、サービスとして起動されるプログラムは極力素早くStartServiceCtrlDispatcher関数を呼び出せと記述されていることから、それ以外の処理は全く行っていない。
ServiceMain関数
ServiceMain関数は名前の通りサービスを提供する処理の本体を構成する。
通常であればこの関数、あるいはここから呼び出された先の関数で、TCPやUDPのポートを開いて、クライアントからの接続を待ち受け、要求を片付けるという処理を行うことになる。
だが、ここでは、外部プログラム(すなわち、Javaで作られた本体のプログラム)を起動して、そのプロセスの終了を待つ、という処理を行うことになる。
重要なのは、サービスが停止されるまでこの関数を抜けてはならないということである。よって、上記のプログラムでは、セマフォを作成して待ち合わせを行っている。
具体的には下記の処理を行っている。
- RegisterServiceCtrlHandler関数を呼び出して、サービス名とそのサービスに対するイベントハンドラの設定を行う。PCの電源断やサービスの停止・再起動などのイベントが発生すると、RegisterServiceCtrlHandler関数で登録したイベントハンドラが飛び出されることになる。
- 待ち合わせ用のセマフォを構築する。このセマフォにより、処理を終了するべき時まで待ち合わせを行う。
- SetServiceStatus関数を呼び出し、サービス制御マネージャに処理状況を通知しつつ、サーバ用プロセスを起動する。上記のプログラムでは「起動中」及び「起動完了」の状況通知を行っている。
- セマフォにより待ち合わせを行う。セマフォの解放は、RegisterServiceCtrlHandler関数で登録したイベントハンドラ(Handler関数)により行われる。
- サーバ用の子プロセス(すなわちJavaで作られた本体のプログラム)を終了させる。
- SetServiceStatus関数により、サービス制御マネージャにサービスが停止された旨の通知を行う。
- ServiceMain関数を終了する。
何度もいうが、ServiceMain関数を抜けるときはサービスが停止されるとき、すなわちユーザが手動でサービスを停止したか、あるいはPCの電源が落とされるときである。Javaのプログラムを起動したら、それで満足してServiceMain関数を抜けてしまってはいけない。
Handler関数
Handler関数内では、サービス制御マネージャから通知されたイベントに対応するための処理を記述する。
上記の例ではとりあえずSERVICE_CONTROL_STOPとSERVICE_CONTROL_SHUTDOWN、すなわち、サービスを停止させるべきタイミングだけを捕まえて処理を行っている。
サービスを停止させる処理としては、ServiceMain関数で待ち合わせているスレッドを再開させる処理のみを行っている。そうすることで、実際にサービスを停止する処理(すなわちJavaで作られた本体のプログラムを終了する処理)は、ServiceMain関数の方で実行される。
なお、ここで気を付けなければならないのは、Handler関数はServiceMain関数とは異なるスレッドで呼び出されるということである。すなわち、Handler関数はServiceMain関数で実行しているサービスを提供する処理とは非同期に呼び出される可能性があるということである。
Javaで作られたプログラムを起動し、終了するまで待ち合わせるというだけの処理であれば、Handler関数とServiceMain関数が別スレッドで実行されても、それ程事態が複雑化することはないのだが、もっと煩雑な処理を実装しようとした場合には、注意を要する。
また、この関数内でイベントを処理したら、逐一SetServiceStatus関数を呼び出して、サービス制御マネージャにサービスの状況を通知してやらなくてはならない。
StartServer関数
StartServer関数はServiceMain関数から呼び出される、Javaで作られた本体のプログラムを起動する処理を行う関数である。
基本的には新しくプロセスを生成する処理を行っているだけである。
ただし、下記の点に注意する必要がある。
- 子プロセス(Javaで作られた本体のプログラム)に、終了するべきタイミングを通知するためのパイプを構築している。
- Javaのプログラムは、java.exeではなくjavaw.exeを使用する。詳細は知らないが、java.exeを使用した場合、子プロセスとのパイプが構築できなくなる。
子プロセスを終了を通知する方法は、別に強いてパイプである必要はない。しかし、CとJavaとでお手軽にプロセス間通信を行う方法が他に思いつかなかったから、とりあえず個々ではパイプを使用している。
つまり、起動用モジュールは終了するべき時が来たら、Javaで作られた本体のプログラムに向けて"stop"という文字列を終了する。逆に本体の方では、常時パイプを監視していて、"stop"という文字列が送られてきたら、その時点で処理を終了するようにする。
そのために、StartServer関数では起動した子プロセスと通信を行うためのパイプ工事を行っている。
KillServer関数
KillServer関数はServiceMain関数から呼び出される、Javaで作られた本体のプログラムを終了させる処理を行う関数である。
StartServer関数の説明にあるとおり、Javaで作られた本体のプログラムを終了させるためにはパイプに"stop"という文字列を書き込んでやればいい。
OSやCPUに関わらず、コンピュータというものは必ずいつかは電源が落とされるものである。その為、起動中ずっと動作し続けるサーバ用プロセスといえども、いつかは終了しなければならない。
そしてまた、終了するためには終了するべきタイミングを知らなければならない。
外部から終了するべきタイミングを通知する方法はいくらでもあるし、何を使おうが個人の勝手ではあるのだが、とりあえずここではパイプを使用している。
上記StartServer関数関数で述べたように、Javaで作られた本体のプログラムは、起動されると同時にパイプの監視を始め、"stop"という文字列が送られてきたら処理を終了するように実装されている。
下記に、サンプルのプログラムを示す。
package MyService;
import java.net.*;
import java.io.*;
// 標準入力から"stop"と入力されたら、プロセスを終了する。
class MyServiceTerminater extends Thread
{
public void run()
{
try {
BufferedReader rBufferedReader = new BufferedReader(
new InputStreamReader( System.in ) );
while ( true ) {
String line = rBufferedReader.readLine();
if ( line.equals( "stop" ) )
System.exit( 0 ); // 終わらせてしまえ
}
}
catch ( Exception e ) {
e.printStackTrace();
}
}
}
public class MyService
{
public static void main( String args[] )
{
// 標準入力の監視とプロセスの終了を行うスレッドを生成
MyServiceTerminater Terminater = new MyServiceTerminater();
Terminater.start();
// ソケット周辺の処理
while ( true ) {
// 要求を処理する
}
}
}
|
監視するべきパイプは標準入力に割り当てられていることを想定している。
サービス制御マネージャから起動されるCのプログラムと、実際にサービスを提供するJavaのプログラムができれば、とりあえずは実行させることができるはずではあるのだが、しかし、実行させるためにはサービス制御マネージャからCのプログラムを起動してもらわなければならない。
サービスに登録するためには、下記のAPI関数を呼び出す必要がある。
SC_HANDLE CreateService(
SC_HANDLE hSCManager, // SCM データベースのハンドル
LPCTSTR lpServiceName, // 開始したいサービスの名前
LPCTSTR lpDisplayName, // 表示名
DWORD dwDesiredAccess, // サービスのアクセス権のタイプ
DWORD dwServiceType, // サービスのタイプ
DWORD dwStartType, // サービスを開始する時期
DWORD dwErrorControl, // サービスに失敗したときの深刻さ
LPCTSTR lpBinaryPathName,// バイナリファイル名
LPCTSTR lpLoadOrderGroup,// ロード順序を決定するグループ名
LPDWORD lpdwTagId, // タグ識別子
LPCTSTR lpDependencies, // 複数の依存名からなる配列
LPCTSTR lpServiceStartName, // アカウント名
LPCTSTR lpPassword // アカウントのパスワード
);
|
CreateService関数というAPIを呼び出すことにより、起動用モジュールをサービスとして登録することができる。
なお、このCreateService関数はインストール時に一度だけ呼び出せば、それ以降はずっと登録されっぱなしになるため、それ以降は呼び出す必要が無くなる。
下記に、インストール時に実行するべき処理を示す。
SC_HANDLE handle2;
SC_HANDLE hSCMgr;
hSCMgr = OpenSCManager( NULL, SERVICES_ACTIVE_DATABASE, SC_MANAGER_ALL_ACCESS );
handle2 = CreateService(
hSCMgr,
"MyService",
"MyService",
STANDARD_RIGHTS_REQUIRED | SERVICE_ALL_ACCESS,
SERVICE_WIN32_OWN_PROCESS,
SERVICE_AUTO_START,
SERVICE_ERROR_IGNORE,
"\"C:\\Myservice.exe\"",
NULL, NULL, NULL, NULL, NULL
);
CloseServiceHandle( handle2 );
CloseServiceHandle( hSCMgr );
|
なお、直感的に考えれば上記の処理はインストーラで行いたい所だが、Visual Studio Installerで開発するMSI形式のインストーラーでは任意のCのプログラムを実行させるような方法が見あたらない。
もっと優秀なインストーラーの開発ソフトを用いるか、あるいは上記関数で行うレジストリ設定を直接インストーラーで登録してしまうかの、どちらかしかないようだ。
インストーラーで行わないとすると、プログラムで明示的に「サービスに登録する」という機能を提供しなければならなくなる。それが良いかどうかは、開発する人の考え方に依るのだろう。