rustによるwindows service で動作するアプリケーション実装
rustでwindows serviceで動作するアプリケーションを実装する。
利用crate、serviceの制御方法などをメモします。
利用crate
windows-service crateが便利なので、これを全面的に利用する。
serviceへのinstall / uninstall 実装
公式リポジトリのexampleを参照して、基本的にはサンプルをそのまま。
static SERVICE_NAME: &'static str = "it_agent"; //登録する serviceの名前
#[cfg(windows)]
pub fn install_service() -> windows_service::Result<()> {
use std::ffi::OsString;
use windows_service::{
service::{ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType},
service_manager::{ServiceManager, ServiceManagerAccess},
};
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;
// This example installs the service defined in `examples/ping_service.rs`.
// In the real world code you would set the executable path to point to your own binary
// that implements windows service.
let service_binary_path = ::std::env::current_exe()
.unwrap()
.with_file_name("C:\\Project\\it_agent\\target\\release\\it_agent.exe");
let service_info = ServiceInfo {
name: OsString::from(SERVICE_NAME),
display_name: OsString::from("IT Agent"), //serviceの表示名
service_type: ServiceType::OWN_PROCESS,
// start_type: ServiceStartType::OnDemand,
start_type: ServiceStartType::AutoStart,
error_control: ServiceErrorControl::Normal,
executable_path: service_binary_path,
launch_arguments: vec!["--mode".into(), "run_service".into()], //service run時、binaryを実行する時のオプションを指定する。これは、xxxxx.exe --mode run_service の例です。
dependencies: vec![],
account_name: None, // run as System
account_password: None,
};
let service = service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?;
service.set_description("Windows service example from windows-service-rs")?;
Ok(())
}
#[cfg(windows)]
pub fn uninstall_service() -> windows_service::Result<()> {
use std::{
thread::sleep,
time::{Duration, Instant},
};
use windows_service::{
service::{ServiceAccess, ServiceState},
service_manager::{ServiceManager, ServiceManagerAccess},
};
use windows_sys::Win32::Foundation::ERROR_SERVICE_DOES_NOT_EXIST;
let manager_access = ServiceManagerAccess::CONNECT;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;
let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE;
let service = service_manager.open_service(SERVICE_NAME, service_access)?;
// The service will be marked for deletion as long as this function call succeeds.
// However, it will not be deleted from the database until it is stopped and all open handles to it are closed.
service.delete()?;
// Our handle to it is not closed yet. So we can still query it.
if service.query_status()?.current_state != ServiceState::Stopped {
// If the service cannot be stopped, it will be deleted when the system restarts.
service.stop()?;
}
// Explicitly close our open handle to the service. This is automatically called when `service` goes out of scope.
drop(service);
// Win32 API does not give us a way to wait for service deletion.
// To check if the service is deleted from the database, we have to poll it ourselves.
let start = Instant::now();
let timeout = Duration::from_secs(5);
while start.elapsed() < timeout {
if let Err(windows_service::Error::Winapi(e)) =
service_manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS)
{
if e.raw_os_error() == Some(ERROR_SERVICE_DOES_NOT_EXIST as i32) {
println!("ping_service is deleted.");
return Ok(());
}
}
sleep(Duration::from_secs(1));
}
println!("ping_service is marked for deletion.");
Ok(())
}
serviceの処理本体
仕様
・プログラムの処理は、serviceの停止メッセージを受け取って処理を停止しなくてはいけない。
・今回のサンプルの仕様として、1秒おき、5秒おき、20秒おきに実行するプロセスがあるとする。
(ここではファイルに書き込みを実施)
参考サイト
windows_service crateのexample
今回、仕様に数秒おきに別のプロセスを実行する必要があるので、
複数スレッドのchannel処理に便利なcrossbeam-cannel crateを利用した。
service実行時、このrun() 関数を呼ぶように実装する。
mainの例
※実行時のオプション(args)で処理を分ける
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(short, long)]
mode: String,,
}
fn main() {
let args = Args::parse();
match args.mode.as_str() {
"run_service" => {
let result = on_windows::services::run_service();
match result {
Ok(_) => {
println!("Service run successfully");
}
Err(e) => {
println!("Error running service: {:?}", e);
}
}
}
"install" => {
let result = on_windows::services::install_service();
match result {
Ok(_) => {
println!("Service installed successfully");
}
Err(e) => {
println!("Error installing service: {:?}", e);
}
}
}
"uninstall" => {
let result = on_windows::services::uninstall_service();
match result {
Ok(_) => {
println!("Service uninstalled successfully");
}
Err(e) => {
println!("Error uninstalling service: {:?}", e);
}
}
}
}
}
service実行時の処理例
#[cfg(windows)]
use std::fs::File;
use crate::constants::SERVICE_NAME;
use std::io::Write;
// use std::sync::mpsc::{Receiver, Sender};
use std::thread;
use std::{
ffi::OsString,
net::{IpAddr, SocketAddr, UdpSocket},
sync::{mpsc, Arc, Mutex},
time::{self, Duration, Instant},
};
static NTHREADS: i32 = 3;
use crossbeam_channel::{bounded, select, tick, unbounded, Receiver, Sender};
use windows_service::{
define_windows_service,
service::{
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus,
ServiceType,
},
service_control_handler::{self, ServiceControlHandlerResult},
service_dispatcher, Result,
};
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
pub fn run() -> Result<()> {
// Register generated `ffi_service_main` with the system and start the service, blocking
// this thread until the service is stopped.
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
}
// Generate the windows service boilerplate.
// The boilerplate contains the low-level service entry function (ffi_service_main) that parses
// incoming service arguments into Vec<OsString> and passes them to user defined service
// entry (my_service_main).
define_windows_service!(ffi_service_main, my_service_main);
// Service entry function which is called on background thread by the system with service
// parameters. There is no stdout or stderr at this point so make sure to configure the log
// output to file if needed.
pub fn my_service_main(_arguments: Vec<OsString>) {
if let Err(_e) = run_service() {
// Handle the error, by logging or something.
}
}
fn write_log(file: &str, msg: &str) {
if std::fs::metadata(file).is_err() {
std::fs::File::create(file);
}
let mut f = std::fs::File::options().append(true).open(file).unwrap();
f.write_all((msg.to_string() + "\n").as_bytes()).unwrap();
}
pub fn run_service() -> Result<()> {
let start = Instant::now();
let update_minute = tick(Duration::from_secs(1));
let update_5minutes = tick(Duration::from_secs(5));
let update_20minutes = tick(Duration::from_secs(20));
// Create a channel to be able to poll a stop event from the service worker loop.
// let (shutdown_tx, shutdown_rx) = mpsc::channel();
let (shutdown_tx, shutdown_rx) = unbounded();
// Define system service event handler that will be receiving service events.
let event_handler = move |control_event| -> ServiceControlHandlerResult {
match control_event {
// Notifies a service to report its current status information to the service
// control manager. Always return NoError even if not implemented.
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
// Handle stop
ServiceControl::Stop => {
shutdown_tx.send(()).unwrap();
ServiceControlHandlerResult::NoError
}
_ => ServiceControlHandlerResult::NotImplemented,
}
};
// Register system service event handler.
// The returned status handle should be used to report service status changes to the system.
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?;
// Tell the system that service is running
status_handle.set_service_status(ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::Running,
controls_accepted: ServiceControlAccept::STOP,
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
})?;
////////////////////////////////////////////////// -------------------------
loop {
select! {
recv(update_minute) -> _ => {
write_log("log1.txt", "1 minute.");
}
recv(update_5minutes) -> _ => {
write_log("log5.txt", "5 minute.");
}
recv(update_20minutes) -> _ => {
write_log("log20.txt", "20 minute.");
//async処理を入れたい場合の例として、tokio runtimeのblock_onを利用
let rt = Runtime::new().unwrap();
// Spawn a blocking function onto the runtime
rt.block_on(async {
match system_info::info::get_system_info().await {
Ok(info) => {
}
Err(e) => {
write_log("error.txt", &e.to_string());
}
}
});
}
recv(shutdown_rx) -> _ => {
write_log("goodbye.txt", "Goodbye.");
break;
}
}
}
// Tell the system that service has stopped.
status_handle.set_service_status(ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::Stopped,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
})?;
Ok(())
}
ちなみに、上記コードで、mainをasyncにすると、windows serviceで実行する分には問題発生しないが、
直接cargo runや、buildされたバイナリを叩くと、エラーが発生する。
#[tokio::main]
async fn main() {
エラー文をそのまま訳すと、
tokioのthread中に、別のtokioによるthreadのランタイムを開始できない、とのこと。
panic_info! at:"panicked at 'Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.', C:\\Users\\xxxxxxx\\.cargo\\registry\\src\\github.com-1ecc6299db9ec823\\tokio-1.26.0\\src\\runtime\\scheduler\\multi_thread\\mod.rs:65:25"
thread 'main' panicked at 'Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.', C:\Users\xxxxxxx\.cargo\registry\src\github.com-1ecc6299db9ec823\tokio-1.26.0\src\runtime\scheduler\multi_thread\mod.rs:65:25
さらに、なぜtokioのblock_onを使っているかというと、
最初、 futures::executor::block_onを使っていたところ、
windows serviceから実行する際にのみ、以下のエラーが発生したため。
そのまま訳すと、
Tokio 1.xランタイムのコンテキスト外で呼び出されたため、リアクターが実行されていないことが原因。つまり、Tokio 1.xランタイム内で呼び出す必要があるとのこと。
この辺理解が浅いので、取り急ぎ、言われるがままにtokioで実装することでクリアした。
/rustc/8460ca823e8367a30dda430efda790588b8c84d3\library\core\src\ops\function.rs:250:5
panicked at 'there is no reactor running, must be called from the context of a Tokio 1.x runtime', /rustc/8460ca823e8367a30dda430efda790588b8c84d3\library\core\src\ops\function.rs:250:5
異常系
serviceが停止できないとき、強制停止する
rust側から、どのように制御するか? が残タスク。
powershellからの対応は以下の通り
#サービス情報を表示
PS C:\Users\user> sc queryex it_agent
SERVICE_NAME: it_agent
TYPE : 10 WIN32_OWN_PROCESS
STATE : 2 START_PENDING
(NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x7d0
PID : 1288
FLAGS :
PS C:\Users\user> taskkill /f /pid 1288
Thx god thats crazy