kotlin/nativeでWindowsサービス
世界の全てはkotlinで書かれるべきです。これは世界の約束です。
かようにkotlinを偏愛しているが故に、Windowsサービスもkotlinで書いてしまうわけです。これが自然の摂理です。
【結論】kotlin/nativeで書いて何が嬉しかったか?
いやー、びっくりするくらい書きにくかったですね。
WindowsAPIにポインタを渡すんで、メモリのスコープを意識して書かないといけない。考え方としては、memScoped
がスタックにメモリを積んでいっているイメージなんですかね。
どうしてもグローバル変数的なものを使いたかったらnativeHeap.alloc
とか使う感じですか。勿論ちゃんとfree
しないといけないわけですけど。今回は、SERVICE_TABLE_ENTRY
という構造体をヒープに置かないとマズイんじゃないかと悩みましてですね、最初はそう書いてたんですが、動きを見てるとmemScoped
でいけるじゃん、と。
あとkotlinなんで型が厳密ですよね。ポインタなんか所詮は32ビットの整数値だろ、何にでもキャストしちゃえとかいう杜撰な考え方は一切許容してくれません。
SERVICE_TABLE_ENTRY構造体の宣言は以下のようになっています。
typedef struct _SERVICE_TABLE_ENTRYW {
LPWSTR lpServiceName;
LPSERVICE_MAIN_FUNCTIONW lpServiceProc;
} SERVICE_TABLE_ENTRYW, *LPSERVICE_TABLE_ENTRYW;
これのLPSERVICE_MAIN_FUNCTIONW
の宣言がこれ。
void LpserviceMainFunctionw(
[in] DWORD dwNumServicesArgs,
[in] LPWSTR *lpServiceArgVectors
)
これの第二引数のLPWSTR*
ですよ。CPointer<LPWSTRVar>
でいいように思いますが、ポインタって基本的にNULLを許容するわけです。そうです、CPointer<LPWSTRVar>?
としてやらないとダメなわけですね。
で、Javaの資産とか使えるはずもないわけですよ。ログファイルを出力するにも、APIぶっ叩いてファイルアクセスしないといけないわけで、そりゃ流石に面倒だなあ、と。
というわけで、さすがにそこは↑を使わせていただきましたとさ。
で、何が嬉しかったのか?
まあいいじゃねえかそんな事は。
プロジェクトの作成
さっさと本題に行きましょう。Kotlin Multipllatform
からNative Application
を選択します。
Finish
とすると……
こんなんですね。
build.gradle.ktsに依存関係を追加
で、ファイルアクセスのためにbuild.gradle.ktsをちょっといじります。
変更点は一番下のsourceSets
のとこだけです。
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は関数ポインタです。
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
})
}
で、これを怒られないように書くとこうなります。
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)
}
}
で、handlerEx
とserviceMain
の中身を書いてやればいいわけですね。
ServiceMainの実装
SERVICE_STATUS構造体の中身を詰め詰めして、SetServiceStatus
でサービスの状態を更新してあげればいいわけです。serviceMainはサービス開始時に呼び出されるのでまずはサービス開始待ち状態に設定します。
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)
// このあと必要な初期処理
今回は特に初期処理はないのでそのまま開始状態にしてしまいます。
// さっき作った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で切り分けてそれぞれの仕事をさせればいいわけですね。
@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です。
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にも置いときます。