ホーム 主筆 その他ソフト その他情報 Syuhitu.org English

Windows関連

スクリーンセーバー作成法

半透明ウインドウの性能

bootfont.bin

キャビネット形式

ウインドウスタイルをいじる

Java製ソフトをServiceに登録する

イベントログにメッセージを出力する

コントロールパネルにアイコンを追加する

スクリプトによる拡張1

スクリプトによる拡張2

ガジェットの作成

大容量メモリ

メモリ搭載量の下限に挑む

スパースファイルにする

表示されるアイコンの種類を調べてみた

メモリマップIOとエラー処理

ファイルを作る順番と速度の関係

Cryptography API: Next Generationを使う

Windows 10のアクセントカラー

iSCSIディスクにバックアップを取る

サーバプロセスを分離して実装する

サーバプロセスを分離して実装する - F#

レジストリに大量に書き込む

Solaris関連

OpenGL

Solaris設定

ディレクトリの読み込み

主筆プラグイン開発

マルチスレッドでの開発

door

音を出す

Blade100の正しい虐め方

パッケージの作成

画像入出力

BMPファイル

ICOファイル

ANIファイル

JPEGファイル

減色アルゴリズム

減色アルゴリズムの並列化

その他アルゴリズムなど

自由軸回転

Base64

文字列操作

CPU利用率の取得

正規表現ライブラリ

メタボールを作る

メタボールを作る2

正規表現とNFA・DFA

C言語の構文解析

液晶ディスプレイを解体してみた

iSCSIの理論と実装

単一フォルダにファイルを沢山作る

USB-HUBのカスケード接続

SafeIntの性能

VHDファイルのフォーマット

USBメモリに書き込み続けてみた

Java製ソフトをWindowsのServiceに登録

2005年11月13日公開

PCの起動と同時にプロセスを上げる

Windowsで、電源が投入されると同時に(すなわちWindowsが起動されると同時に)プログラムを起動するには、一般的に下記の三つの方法がある。

  1. スタートアップにショートカットを登録する。

  2. レジストリに登録する。

  3. サービスに登録する。

上記の内、1と2は、誰かがログインしないとプロセスが起動されない。しかし、ごく普通のショートカットを配置したり、あるいは起動時のパス名をレジストリに登録するだけで自動的に起動させることが可能となる。

サービスに登録すれば、誰かがログインしなくてもPCが起動すると同時にプロセスを起動することが可能となる。しかし、サービスに登録するには、プログラムをサービスとして起動されるための特殊な作りにしてやる必要がある。

でもって、サービスとして起動されるためのインタフェースはC言語のものである。すなわち、サービスとして起動されるプログラムはCもしくはC++(あるいはC言語のインタフェースを提供することができるその他の言語)である必要がある。

だから、一般的にはJavaで作ったプログラムをサービスに登録することはできない。

Javaで作ったプログラムをサービスに登録する

しかし、サービスとして起動される部分だけをCで作ってやって、そこから目的のJavaで作ったプログラムを起動するようにしてやれば、必ずしもJavaで作ったプログラムをサービスとして起動させることができない訳ではない。

このページでは、Javaで作ったプログラムをサービスとして登録する方法について説明する。なお、サービスに登録するという行為は、それ自体が完全にWindowsに依存するものであるため、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で作られた本体のプログラム)を起動して、そのプロセスの終了を待つ、という処理を行うことになる。

重要なのは、サービスが停止されるまでこの関数を抜けてはならないということである。よって、上記のプログラムでは、セマフォを作成して待ち合わせを行っている。

具体的には下記の処理を行っている。

  1. RegisterServiceCtrlHandler関数を呼び出して、サービス名とそのサービスに対するイベントハンドラの設定を行う。PCの電源断やサービスの停止・再起動などのイベントが発生すると、RegisterServiceCtrlHandler関数で登録したイベントハンドラが飛び出されることになる。
  2. 待ち合わせ用のセマフォを構築する。このセマフォにより、処理を終了するべき時まで待ち合わせを行う。
  3. SetServiceStatus関数を呼び出し、サービス制御マネージャに処理状況を通知しつつ、サーバ用プロセスを起動する。上記のプログラムでは「起動中」及び「起動完了」の状況通知を行っている。
  4. セマフォにより待ち合わせを行う。セマフォの解放は、RegisterServiceCtrlHandler関数で登録したイベントハンドラ(Handler関数)により行われる。
  5. サーバ用の子プロセス(すなわちJavaで作られた本体のプログラム)を終了させる。
  6. SetServiceStatus関数により、サービス制御マネージャにサービスが停止された旨の通知を行う。
  7. 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で作られた本体のプログラムを起動する処理を行う関数である。

基本的には新しくプロセスを生成する処理を行っているだけである。

ただし、下記の点に注意する必要がある。

  1. 子プロセス(Javaで作られた本体のプログラム)に、終了するべきタイミングを通知するためのパイプを構築している。
  2. Javaのプログラムは、java.exeではなくjavaw.exeを使用する。詳細は知らないが、java.exeを使用した場合、子プロセスとのパイプが構築できなくなる。

子プロセスを終了を通知する方法は、別に強いてパイプである必要はない。しかし、CとJavaとでお手軽にプロセス間通信を行う方法が他に思いつかなかったから、とりあえず個々ではパイプを使用している。

つまり、起動用モジュールは終了するべき時が来たら、Javaで作られた本体のプログラムに向けて"stop"という文字列を終了する。逆に本体の方では、常時パイプを監視していて、"stop"という文字列が送られてきたら、その時点で処理を終了するようにする。

そのために、StartServer関数では起動した子プロセスと通信を行うためのパイプ工事を行っている。

KillServer関数

KillServer関数はServiceMain関数から呼び出される、Javaで作られた本体のプログラムを終了させる処理を行う関数である。

StartServer関数の説明にあるとおり、Javaで作られた本体のプログラムを終了させるためにはパイプに"stop"という文字列を書き込んでやればいい。

起動される側のJavaの処理

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のプログラムを実行させるような方法が見あたらない。

もっと優秀なインストーラーの開発ソフトを用いるか、あるいは上記関数で行うレジストリ設定を直接インストーラーで登録してしまうかの、どちらかしかないようだ。

インストーラーで行わないとすると、プログラムで明示的に「サービスに登録する」という機能を提供しなければならなくなる。それが良いかどうかは、開発する人の考え方に依るのだろう。

サービスからの削除方法

あこぎな会社なら、あえてアンインストールできないようにする、ということもやりかねないが、しかし、一般的にいってそういうことをやる会社は世の中から排除される事になる。

サービスに登録したのなら、ちゃんと登録を削除する機能を提供するべきである。

サービスから登録を削除するには、DeleteService関数というAPI関数を呼び出してやればいい。削除処理を示すと下記の様になる。

SC_HANDLE handle2;
SC_HANDLE hSCMgr;
handle2 = OpenService( hSCMgr, "MyService", DELETE );
DeleteService( handle2 );
CloseServiceHandle( handle2 );
CloseServiceHandle( hSCMgr );

この処理も登録時の処理と同様、アンインストーラーで行わせようとすると、いろいろな制約に引っかかってうまくいかない。

インストール時と同じように何らかの対応を考える必要がある。

 


連絡先 - サイトマップ - 更新履歴
Copyright (C)  2000 - 2016 nabiki_t All Rights Reserved.