Open6

rustによるwindows service で動作するアプリケーション実装

yunayuna

rustでwindows serviceで動作するアプリケーションを実装する。
利用crate、serviceの制御方法などをメモします。

yunayuna

serviceへのinstall / uninstall 実装

公式リポジトリのexampleを参照して、基本的にはサンプルをそのまま。
https://github.com/mullvad/windows-service-rs/tree/main/examples

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(())
}

yunayuna

serviceの処理本体

仕様

・プログラムの処理は、serviceの停止メッセージを受け取って処理を停止しなくてはいけない。
・今回のサンプルの仕様として、1秒おき、5秒おき、20秒おきに実行するプロセスがあるとする。
 (ここではファイルに書き込みを実施)

参考サイト

windows_service crateのexample
https://github.com/mullvad/windows-service-rs/blob/main/examples/ping_service.rs

今回、仕様に数秒おきに別のプロセスを実行する必要があるので、
複数スレッドのchannel処理に便利なcrossbeam-cannel crateを利用した。
https://github.com/crossbeam-rs/crossbeam/tree/master/crossbeam-channel/examples

service実行時、このrun() 関数を呼ぶように実装する。

mainの例

※実行時のオプション(args)で処理を分ける

main.rs

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実行時の処理例

service.rs
#[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
yunayuna

異常系

serviceが停止できないとき、強制停止する
https://tecadmin.net/force-stop-a-service-windows/

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