4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Kotlin/NativeでWindowsサービスを作ってみた

Posted at

kotlin/nativeでWindowsサービス

世界の全てはkotlinで書かれるべきです。これは世界の約束です。
かようにkotlinを偏愛しているが故に、Windowsサービスもkotlinで書いてしまうわけです。これが自然の摂理です。

【結論】kotlin/nativeで書いて何が嬉しかったか?

いやー、びっくりするくらい書きにくかったですね。
WindowsAPIにポインタを渡すんで、メモリのスコープを意識して書かないといけない。考え方としては、memScopedがスタックにメモリを積んでいっているイメージなんですかね。

どうしてもグローバル変数的なものを使いたかったらnativeHeap.allocとか使う感じですか。勿論ちゃんとfreeしないといけないわけですけど。今回は、SERVICE_TABLE_ENTRYという構造体をヒープに置かないとマズイんじゃないかと悩みましてですね、最初はそう書いてたんですが、動きを見てるとmemScopedでいけるじゃん、と。

あとkotlinなんで型が厳密ですよね。ポインタなんか所詮は32ビットの整数値だろ、何にでもキャストしちゃえとかいう杜撰な考え方は一切許容してくれません。

SERVICE_TABLE_ENTRY構造体の宣言は以下のようになっています。

excerpt.cpp
typedef struct _SERVICE_TABLE_ENTRYW {
  LPWSTR                   lpServiceName;
  LPSERVICE_MAIN_FUNCTIONW lpServiceProc;
} SERVICE_TABLE_ENTRYW, *LPSERVICE_TABLE_ENTRYW;

これのLPSERVICE_MAIN_FUNCTIONWの宣言がこれ。

excerpt.cpp
void LpserviceMainFunctionw(
  [in] DWORD dwNumServicesArgs,
  [in] LPWSTR *lpServiceArgVectors
)

これの第二引数のLPWSTR*ですよ。CPointer<LPWSTRVar>でいいように思いますが、ポインタって基本的にNULLを許容するわけです。そうです、CPointer<LPWSTRVar>?としてやらないとダメなわけですね。

で、Javaの資産とか使えるはずもないわけですよ。ログファイルを出力するにも、APIぶっ叩いてファイルアクセスしないといけないわけで、そりゃ流石に面倒だなあ、と。

というわけで、さすがにそこは↑を使わせていただきましたとさ。
で、何が嬉しかったのか?

まあいいじゃねえかそんな事は。

プロジェクトの作成

image.png
さっさと本題に行きましょう。Kotlin MultipllatformからNative Applicationを選択します。

image.png

Finishとすると……

image.png

こんなんですね。

build.gradle.ktsに依存関係を追加

で、ファイルアクセスのためにbuild.gradle.ktsをちょっといじります。
変更点は一番下のsourceSetsのとこだけです。

build.gradle.kts
plugins {
    kotlin("multiplatform") version "1.8.20"
}

group = "org.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

kotlin {
    val hostOs = System.getProperty("os.name")
    val isMingwX64 = hostOs.startsWith("Windows")
    val nativeTarget = when {
        hostOs == "Mac OS X" -> macosX64("native")
        hostOs == "Linux" -> linuxX64("native")
        isMingwX64 -> mingwX64("native")
        else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
    }

    nativeTarget.apply {
        binaries {
            executable {
                entryPoint = "main"
            }
        }
    }
    sourceSets {
        commonMain {
            dependencies {
                implementation("me.archinamon:file-io:1.3.6")
                implementation("me.archinamon:file-io-mingwx64:1.3.6")
            }
        }
    }
}

ではMain.ktに全部書いちゃいましょう。mainから。
実際にこんな事を書きたいわけですね。handlerExとserviceMainは関数ポインタです。

こう書きたい.kt
const val SERVICE_NAME = "ko-service"

var hServiceStatus: SERVICE_STATUS_HANDLE? = null

fun handlerEx(dwControl: DWORD, dwEventType: DWORD, lpEventData: LPVOID, lpContext: LPVOID)
  : DWORD {
  return 0
}

fun serviceMain(dwArgc: DWORD, pszArgv: CPointer<LPWSTR>) {
    RegisterServiceCtrlHandlerEx(SERVICE_NAME, handlerEx, NULL)
}

fun main() {
    StartServiceCtrlDispatcher(SERVICE_TABLE_ENTRY().apply {
            lpServiceName = SERVICE_NAME
            lpServiceProc = serviceMain
    })
}

で、これを怒られないように書くとこうなります。

Main1.kt
const val SERVICE_NAME = "ko-service"

var hServiceStatus: SERVICE_STATUS_HANDLE? = null

@ExperimentalUnsignedTypes
fun handlerEx(dwControl: DWORD, dwEventType: DWORD, lpEventData: LPVOID?, lpContext: LPVOID?)
 : DWORD {
    return 0u
}

@ExperimentalUnsignedTypes
fun serviceMain(dwArgc: DWORD, pszArgv: CPointer<LPWSTRVar>?) {
    memScoped {
        hServiceStatus = RegisterServiceCtrlHandlerEx!!(
            SERVICE_NAME.wcstr.ptr, staticCFunction(::handlerEx), NULL)
    }
}

@ExperimentalUnsignedTypes
fun main() {
    memScoped {
        StartServiceCtrlDispatcher!!(alloc<SERVICE_TABLE_ENTRYW>().apply {
            lpServiceName = SERVICE_NAME.wcstr.ptr
            lpServiceProc = staticCFunction(::serviceMain)
        }.ptr)
    }
}

で、handlerExserviceMainの中身を書いてやればいいわけですね。

ServiceMainの実装

SERVICE_STATUS構造体の中身を詰め詰めして、SetServiceStatusでサービスの状態を更新してあげればいいわけです。serviceMainはサービス開始時に呼び出されるのでまずはサービス開始待ち状態に設定します。

サービス開始待ち.kt
val ss = alloc<SERVICE_STATUS>().apply {
    dwServiceType = SERVICE_WIN32_OWN_PROCESS.toUInt()
    dwWin32ExitCode = NO_ERROR.toUInt()

    dwCurrentState = SERVICE_START_PENDING.toUInt() // サービス開始待ち
    dwCheckPoint = 1u    // チェックポイントを0以外に設定
    dwWaitHint = 3000u   // 初期化処理に必要なミリ秒を充分確保
                         // この時間が経過すると起動失敗と判断される 
    dwControlsAccepted = SERVICE_ACCEPT_STOP.toUInt() // 「停止」を受け入れる
}

// ハンドルとSERVICE_STATUSのポインタを渡して状態通知
SetServiceStatus(hServiceStatus, ss.ptr)

// このあと必要な初期処理

今回は特に初期処理はないのでそのまま開始状態にしてしまいます。

サービス開始.kt
// さっき作ったSERVICE_STATUS構造体を再利用
ss.apply {
    dwCurrentState  = SERVICE_RUNNING.toUInt() // サービス開始状態
    dwCheckPoint = 0u // チェックポイントを0にして保留処理がない事を通知
    dwWaitHint = 0u 
    // 一時停止(再開)と停止を受け入れる
    dwControlsAccepted = SERVICE_ACCEPT_PAUSE_CONTINUE.or(SERVICE_ACCEPT_STOP).toUInt()
}

// ハンドルとSERVICE_STATUSのポインタを渡して状態通知
SetServiceStatus(hServiceStatus, ss.ptr)

while(true) {
    // サービスのメイン処理
}

HandlerExの実装

第一引数のdwControlに「停止」「一時停止」「再開」といった情報が入ってきます。
なのでwhenで切り分けてそれぞれの仕事をさせればいいわけですね。

Main.kt
@ExperimentalUnsignedTypes
fun serviceControlStop() {
  // dwCurrentStateをSERVICE_STOP_PENDINGで通知し、
  // 終了処理をしたあとでSERVICE_STOPPEDを通知
}

@ExperimentalUnsignedTypes
fun serviceControlPause() {
  // dwCurrentStateをSERVICE_PAUSE_PENDINGで通知し、
  // 終了処理をしたあとでSERVICE_PAUSEDを通知
}

@ExperimentalUnsignedTypes
fun serviceControlContinue() {
  // dwCurrentStateをSERVICE_START_PENDINGで通知し、
  // 終了処理をしたあとでSERVICE_RUNNINGを通知
}

@ExperimentalUnsignedTypes
fun handlerEx(dwControl: DWORD, dwEventType: DWORD, lpEventData: LPVOID?, lpContext: LPVOID?)
 : DWORD {
    when (dwControl.toInt()) {
        SERVICE_CONTROL_STOP -> serviceControlStop()
        SERVICE_CONTROL_PAUSE -> serviceControlPause()
        SERVICE_CONTROL_CONTINUE -> serviceControlContinue()
        else -> return ERROR_CALL_NOT_IMPLEMENTED.toUInt()
    }
    return NO_ERROR.toUInt()
}

で、完成したものがこちら

exeがある場所に、毎秒ログを書くだけのサービスの完成。
フラグ処理がダサい感じがしますが、今日のところは見逃してもらってOKです。

Main.kt
import kotlinx.cinterop.invoke
import kotlinx.cinterop.*
import me.archinamon.fileio.*
import platform.windows.*

const val SERVICE_NAME = "ko-service"

var running = true
var active = true
var fin = false

var hServiceStatus: SERVICE_STATUS_HANDLE? = null

@ExperimentalUnsignedTypes
fun serviceControlStop(ss: SERVICE_STATUS) {
    log("stop")

    ss.dwCurrentState = SERVICE_STOP_PENDING.toUInt()
    SetServiceStatus(hServiceStatus, ss.ptr)

    running = false

    while(!fin) {
        Sleep(1000)
    }

    ss.dwCurrentState = SERVICE_STOPPED.toUInt()
    SetServiceStatus(hServiceStatus, ss.ptr)
}

@ExperimentalUnsignedTypes
fun serviceControlPause(ss: SERVICE_STATUS) {
    log("pause")

    ss.dwCurrentState = SERVICE_PAUSE_PENDING.toUInt()
    SetServiceStatus(hServiceStatus, ss.ptr)

    active = false

    ss.dwCurrentState = SERVICE_PAUSED.toUInt()
    ss.dwControlsAccepted = SERVICE_STOPPED.or(SERVICE_ACCEPT_PAUSE_CONTINUE).toUInt()

    SetServiceStatus(hServiceStatus, ss.ptr)
}

@ExperimentalUnsignedTypes
fun serviceControlContinue(ss: SERVICE_STATUS) {
    log("continue")

    ss.dwCurrentState = SERVICE_START_PENDING.toUInt()

    SetServiceStatus(hServiceStatus, ss.ptr)

    active = true

    ss.dwCurrentState = SERVICE_RUNNING.toUInt()
    ss.dwControlsAccepted = SERVICE_STOPPED.or(SERVICE_ACCEPT_PAUSE_CONTINUE).toUInt()

    SetServiceStatus(hServiceStatus, ss.ptr)
}

@ExperimentalUnsignedTypes
fun handlerEx(
    dwControl: DWORD,
    @Suppress("UNUSED_PARAMETER") dwEventType: DWORD,
    @Suppress("UNUSED_PARAMETER") lpEventData: LPVOID?,
    @Suppress("UNUSED_PARAMETER") lpContext: LPVOID? ): DWORD {

    memScoped {
        val ss = alloc<SERVICE_STATUS>().apply {
            dwServiceType = SERVICE_WIN32_OWN_PROCESS.toUInt()
            dwWin32ExitCode = NO_ERROR.toUInt()
            dwServiceSpecificExitCode = 0u
            dwCheckPoint = 0u
            dwWaitHint = 0u
            dwControlsAccepted = SERVICE_ACCEPT_STOP.toUInt()
        }

        when (dwControl.toInt()) {
            SERVICE_CONTROL_STOP -> serviceControlStop(ss)
            SERVICE_CONTROL_PAUSE -> serviceControlPause(ss)
            SERVICE_CONTROL_CONTINUE -> serviceControlContinue(ss)
            else -> return ERROR_CALL_NOT_IMPLEMENTED.toUInt()
        }
    }
    return NO_ERROR.toUInt()
}

@ExperimentalUnsignedTypes
fun serviceMain(@Suppress("UNUSED_PARAMETER") dwArgc: DWORD,
                @Suppress("UNUSED_PARAMETER") pszArgv: CPointer<LPWSTRVar>?) {
    memScoped {
        RegisterServiceCtrlHandlerEx!!(
            SERVICE_NAME.wcstr.ptr, staticCFunction(::handlerEx), NULL)?.let {
            hServiceStatus = it

            log("pending...")
            val ss = alloc<SERVICE_STATUS>().apply {
                dwServiceType = SERVICE_WIN32_OWN_PROCESS.toUInt()
                dwWin32ExitCode = NO_ERROR.toUInt()

                dwCurrentState = SERVICE_START_PENDING.toUInt()
                dwCheckPoint = 1u
                dwWaitHint = 3000u
                dwControlsAccepted = SERVICE_ACCEPT_STOP.toUInt()
            }
            SetServiceStatus(hServiceStatus, ss.ptr)

            log("running")
            ss.apply {
                dwCurrentState  = SERVICE_RUNNING.toUInt()
                dwCheckPoint = 0u
                dwWaitHint = 0u
                dwControlsAccepted = SERVICE_ACCEPT_PAUSE_CONTINUE.or(SERVICE_ACCEPT_STOP).toUInt()
            }
            SetServiceStatus(hServiceStatus, ss.ptr)

            while (running) {
                if(active)
                    log("$SERVICE_NAME is running.")

                Sleep(1000)
            }

            log("End of Kotlin Service.")
            fin = true

        } ?: log("RegisterServiceCtrlHandler failed. ${GetLastError()}")
    }
}

fun modulePath() =
    memScoped {
        allocArray<WCHARVar>(256).apply {
            GetModuleFileName!!(null, this, 256u)
            PathRemoveFileSpec!!(this)
        }.toKString()
    }

const val CRLF = "\r\n"
fun log(str: String) {
    println(str)
    File("${modulePath()}/ko-service.log").apply {
        if(!exists())
            createNewFile()
        appendText("$str$CRLF")
    }
}

@ExperimentalUnsignedTypes
fun main() {
    memScoped {
        StartServiceCtrlDispatcher!!(alloc<SERVICE_TABLE_ENTRYW>().apply {
            lpServiceName = SERVICE_NAME.wcstr.ptr
            lpServiceProc = staticCFunction(::serviceMain)
        }.ptr)
    }
}

サービスを登録する

ビルドするとプロジェクトルートのbuild/bin/native/releaseExecutableにexeが出来ます。
あとはPowerShellを管理者モードで立ち上げてコマンドで登録します。パスは適当に読み替えてください。

sc create KO_SERVICE binPath= C:\git\ko-service\build\bin\native\releaseExecutable\ko-service.exe

サービスの開始

net start KO_SERVICE

サービスの削除

sc delete KO_SERVICE

コツを掴めばある程度スルスル書けるようにはなったが……

kotlinというよりはWindowsAPIと格闘している感じになっちゃいましたかね。
わざわざkotlinで書くために苦労しているという部分も否めないところがありますが。

気になったのはリテラル文字列へのポインタですか。

SERVICE_NAME.wcstr.ptrを複数使ってますが、これ同じポインタを指してるんだろうかとか。そもそもC/C++のリテラル文字列へのポインタを表現する方法はあるのかないのか。無きゃ無いでいいんですけどね。

わざわざKotlin/NativeでWindowsサービスを作る意味というのはあまりないのかも知れませんが、意味のないことを繰り返す事こそが人生の本質なのかも知れません。何言ってんだろうね。

というわけでGitHubにも置いときます。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?