Java製ソフトをWindowsのServiceに登録
[1] 主筆嬢 05/11/12 19:56
Javaでサーバープロセスになるプログラムを作ったんだけど、Windowsが起動すると同時にこのプロセスが上がるようにするにはどうしたらいい?
「スタートアップ」に登録しても、レジストリの「HKEY_LOCAL_MOCHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run」に登録しても、誰かユーザがログインしないとプロセスが起動されない。
出来ればそうじゃなくって、誰もログインする必要なくプロセスが上がるようにしたいんだけど。
[2] nabiki_t 05/11/12 19:58
それをやるには、WindowsのServiceに登録してやる必要がある。Serviceってのはコントロールパネルの「管理ツール」の中の「サービス」を選択すると表示される画面に出てくる奴だ。
画面を示すと、大体こんな感じだ。

で、この画面の中の「スタートアップの種類」というカラムに「自動」と表示されてる奴は、Windowsが起動したときに一緒に起動される。
[3] 主筆嬢 05/11/12 19:59
ふーん。
やってみる。
[4] 主筆嬢 05/11/12 20:05
いろいろいじってみたけど、新しく登録するにはどこをどうするのかが分からない。
[5] nabiki_t 05/11/12 20:08
残念ながら、「サービス」の画面からプロセスを登録する方法はない。登録するには、WindowsのAPIを呼び出してやらなくてはならない。
それに、呼び出される側のプロセスも、Serviceとして起動される為の、特殊な作りにしてやらなくてはならない。
[6] 主筆嬢 05/11/12 20:09
じゃあ、Javaで作ったプログラムは、Serviceに登録することは出来ないの?
[7] あぼーん 05/11/12 あぼーん
あぼーん
[8] nabiki_t 05/11/12 20:12
いや、全く不可能と言うわけではない。
Serviceに登録するためのプログラム(通常はインストーラか何かになると思う)と、Serviceから呼び出されてJavaのプログラムをキックするプログラムをCか何かで作ってやれば可能だ。
[9] 主筆嬢 05/11/12 20:13
ふーん。
じゃ、全部Javaだけで作成するのは不可能なの?
[10] nabiki_t 05/11/12 20:14
それはやったことがないから分からない。ただ、Javaは一応C関数を呼ぶことが出来るし、Cから呼ばれることも出来るはずだから、もしかしたら、やれば出来るかも知れない。
でも、そんなことをしたって、結局Windowsに依存することになるのだから、無理してJavaで作成するメリットは無いと思う。
[11] 主筆嬢 05/11/12 20:16
あっそう。
で、その「登録するプログラム」と「呼ばれるプログラム」はどうやって作るの?
[12] 広告 05/11/12 20:17
[13] nabiki_t 05/11/12 20:18
じゃあ、順番に。
まず、「登録するプログラム」だが、これは単にCreateServiceという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 // アカウントのパスワード
);
|
まぁ、詳しい説明はMSDNに有るから省略させてもらう。
で、例えば、"c:\MyService.exe"というプログラムを、"MyService"という名前で登録するのなら、大体下記のようになる。
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 );
|
[14] nabiki_t 05/11/12 20:19
で、ついでに、登録したのならその登録を削除する術も必要になるわけで。
登録を削除するには、DeleteServiceという関数を呼び出す。この関数の詳細も、MSDNにその紹介を譲ることにする。
で、上で登録したMyServiceを削除するのなら、大体下記のようになる。
SC_HANDLE handle2;
SC_HANDLE hSCMgr;
handle2 = OpenService( hSCMgr, "MyService", DELETE );
DeleteService( handle2 );
CloseServiceHandle( handle2 );
CloseServiceHandle( hSCMgr );
|
[15] 主筆嬢 05/11/12 20:20
ふーん。
じゃ、その>>13と>>14をやれば、Windowsが起動したときに”C:\MyService.exe”が起動されるようになるというわけ。
だったら、そのMyservice.exeが、私が作ったJavaのプログラムを起動するためのプログラムと言うことになるの?
[16] nabiki_t 05/11/12 20:21
まぁ、そういうことだ。
だが、このダミーのプログラムを作るのが一番やっかいだったりもする。
説明が面倒だから、とりあえずプログラムの例を下記に示す。
#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;
void KillServer( int fdpipe )
{
// 子プロセスを停止する
write( fdpipe, "stop", 4 );
write( fdpipe, "\n", 1 );
}
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];
}
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:
// 待ち合わせているスレッドを動かす
stat.dwCurrentState = SERVICE_STOP_PENDING;
ReleaseSemaphore( glb_Semaphore, 1, NULL );
break;
}
SetServiceStatus( glb_ServiceStatusHandle, &stat);
}
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);
// サーバの終了を待ち合わせる
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;
}
|
[17] 主筆嬢 05/11/12 20:23
これだけ見せられても・・・
[18] nabiki_t 05/11/12 20:24
だから、順を追って説明するって。
まず、main関数だが、これは単にSERVICE_TABLE_ENTRY構造体に値を設定して、StartServiceCtrlDispatcher関数を呼んでるだけだ。
要はサービス名とそれに対応する関数の対応を通知しているだけだ。MSDNの記述によると、起動したらすぐにStartServiceCtrlDispatcherを呼び出せって書いてあるから、おとなしくそれに従っておいた方がいいだろう。
[19] nabiki_t 05/11/12 20:25
次に、ServiceMain関数だが、この関数が実際にサービスを提供する本体部分になる。
通常ならここで、ポートを開いてクライアントからのアクセスを待ち、要求があったらその要求を処理するためのスレッドを生成する、というプログラムが記述されることになる。
だが、ここでは、外部プログラム(Javaで作られた奴)を起動して、そのプロセスの終了を待つ、という処理になる。重要なのは、サービスが停止されるまでこの関数を抜けてはならないと言うことだ。だから、上記のプログラムでは、セマフォを作成して待ち合わせを行っている。
で、この関数内での具体的な処理だが、
まずは、RegisterServiceCtrlHandler関数を呼び出して、サービス名とそのサービスに対するイベントハンドラを設定している。ここで、通知されるイベントというのは、「管理ツール」の「サービス」の画面でユーザが操作を行ったというやつとか、電源が落とされるぞ、とかいうやつだ。
その次に、適宜SetServiceStatus関数を呼び出して状況を通知しつつ、サーバー用プロセスを起動している。一応、上記のプログラムでは、「起動中だぞ」というのと「起動したぞ」というのを通知している。
で、サーバを起動したら、後はサービスが終了するのを待ち合わせる。そして、サービスを終了するべき時期が来たら、生成した子プロセスを殺して、「サーバが停止したぞ」という状況を通知して、ServiceMain関数を抜けている。
[20] nabiki_t 05/11/12 20:29
その次はHandler関数だが、この中では、通知されたイベントに対応する処理を記述すればいい。ここでは、とりあえずSERVICE_CONTROL_STOPとSERVICE_CONTROL_SHUTDOWNだけ、つまりサーバーを停止させると言う奴だけ処理している。
で、通常のサーバ用プログラムならば、グローバル変数にフラグを立てるなりなんなりして、ServiceMain関数のメインルーチンを抜ける様な処理を行うのだろうが、ここでは、ServiceMain内でWaitForSingleObjectによって待ち合わせているスレッドを再開させる為の処理(つまりReleaseSemaphoreの呼び出し)に置き換わっている。
また、この関数内でイベントを処理したら、逐一SetServiceStatusを呼び出してサービスの状況を通知してやらなくてはならない。
[21] nabiki_t 05/11/12 20:31
後はまぁ、サーバ用プロセスを起動する処理と、起動したプロセスを殺す処理だけだ。
[22] 主筆嬢 05/11/12 20:32
ふーん。分かった。
でも、何でStartServer関数内でパイプ工事をやってるの?
[23] nabiki_t 05/11/12 20:34
それは、起動したJavaのプログラムが、標準入力から”stop”という文字列を読み込むと終了するよう作られているから(そういうことを想定しているから)だ。
この辺は、起動されるプログラムを停止させる方法によって、いろいろと変わりうる。まぁ、いわゆるIPC問題だな。
[24] 広告 05/11/12 20:36
[25] 主筆嬢 05/11/12 20:38
それと後、プロセスを起動するとき、なんでspawnの第一引数に_P_NOWAITを指定しているの?
_P_WAITでプロセスが終了するまで待ち合わせるようにしても、いいんじゃない?
[26] nabiki_t 05/11/12 20:40
それでもいいような気もするが、一応、SetServiceStatus関数で、サービス開始時には「開始中」であることを通知し、開始した後に「開始した」と言うことを通知する様にしなければならないことになっているから、それに従ったまでのことだ。
[27] 主筆嬢 05/11/12 20:42
それに最後、何でJavaの処理系としてjava.exeではなくjavaw.exeを使ってるの?
[28] nabiki_t 05/11/12 20:43
それは、java.exeを使うとプロセスを終了させることが出来なくなるからだ。
詳しい理由は知らないが、java.exeを使うとパイプが正しく接続されないらしい。write( fdpipe, "stop", 4 );とやっても、Javaのプログラム側に”stop”と言う文字列が送られないんだ。
[29] 主筆嬢 05/11/12 20:45
ふーん。
あっ、それにもう一つ。
Javaのプログラムの方はどうなってるの?
[30] nabiki_t 05/11/12 20:47
最後じゃなかったのかよ。
まぁいい。じゃ、参考までに。
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 ) {
// 要求を処理する
}
}
}
|
やりたいこととしては、標準入力を監視するスレッドを生成して、文字列”stop”が送られてくるのを待ち合わせる。で、送られてきたらプロセスを終了させる。と言うことだな。
まぁ、もっとも、いきなりSystem.exit( 0 )は野蛮すぎる気もするがな。
[31] 主筆嬢 05/11/12 20:50
ふーん。分かった。
|