🐥

RustでWindowsサービスを作成するメモ

に公開

出来れば常時起動しておきたいコンソールアプリをRustで作って居ました。
ふと思ったのですが、Windowsで常駐させるならサービスじゃぁないかぁと。

そんなわけでWindowsサービスアプリケーション化を行ってみました。
プログラム自体はもっと良い書き方があると思うのですが、自分の備忘録ともしサービス作る人の一助になればと思い公開します。
※修正しました(2024/10/22)

使用パッケージについて

cargo.toml
anyhow = "1.0.89"
clap = { version = "4.5.18", features = ["derive"] }
dotenvy = "0.15.7"
file-rotate = "0.7.6"
log = "0.4.22"
simplelog = "0.12.2"
windows-service = "0.7.0"
windows-sys = "0.59.0"

今回、サービスの登録/解除には windows_service_controller を使い、サービスの実行には windows-service を使っています。
windows-serviceクレートだけでサービスの登録/解除もできるのと、元のプログラムだとReleaseビルドでエラー発生したので修正しました。以下、すべて修正後のものです。

またログの出力にはsimplelogを使っています。今時だとtracingを使うことが多いのかなと思いますが、サービス実行時にログが出力されずsimplelogを使っています。

ソースコード

service_control.rs

[windows-serviceクレートのサンプル](https://github.com/mullvad/windows-service-rs/tree/main/examples)をほぼコピペしてそのままです。
service_control.rs
use log::info;
use std::{ffi::OsString, thread::sleep, time::{Duration, Instant}};
use windows_service::{
    service::{ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState, ServiceType},
    service_manager::{ServiceManager, ServiceManagerAccess},
};
use windows_sys::Win32::Foundation::ERROR_SERVICE_DOES_NOT_EXIST;

// サービス登録
pub fn service_install<S: Into<OsString>>(service_name: &str, args: Vec<S>) -> anyhow::Result<()>{
    let args:Vec<OsString> = args.into_iter().map(|i| i.into() ).collect();

    let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
    let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;

    let service_binary_path = ::std::env::current_exe()
        .unwrap();

    let service_info = ServiceInfo {
        name: OsString::from(service_name),
        display_name: OsString::from(service_name),
        service_type: ServiceType::OWN_PROCESS,
        start_type: ServiceStartType::OnDemand,
        error_control: ServiceErrorControl::Normal,
        executable_path: service_binary_path,
        launch_arguments: args,
        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(OsString::from(service_name))?;

    Ok(())

}

// サービスの登録解除
pub fn service_uninstall(service_name: &str) -> anyhow::Result<()> {

    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) {
                info!("{} is deleted.", service_name);
                return Ok(());
            }
        }
        sleep(Duration::from_secs(1));
    }
    info!("{} is marked for deletion.", service_name);

    Ok(())

}

sample_service.rs

サービスのメイン部分です。loop部分にいい感じにサービスの本体を置いてください。
またこのプログラムで「サービスを終了しました」とログを出力していますが、出力が非同期のせいだと思いますがログが出力される前に終了してしまいました。

sample_service.rs
use std::{
    ffi::OsString,
    sync::mpsc,
    time::Duration,
};
use windows_service::{
    service::{
        ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus,
        ServiceType,
    },
    service_control_handler::{self, ServiceControlHandlerResult},
};

pub const SERVICE_NAME:&str = env!("CARGO_PKG_NAME");

pub fn my_service_main(arguments: Vec<OsString>) {

    if let Err(_e) = run_service(arguments) {
        // Handle error in some way.
    }

}

fn run_service(_: Vec<OsString>) -> Result<(), windows_service::Error> {

    let (shutdown_tx, shutdown_rx) = mpsc::channel();

    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
            }

            // treat the UserEvent as a stop request
            ServiceControl::UserEvent(code) => {
                if code.to_raw() == 130 {
                    shutdown_tx.send(()).unwrap();
                }
                ServiceControlHandlerResult::NoError
            }

            _ => ServiceControlHandlerResult::NotImplemented,
        }
    };

    // Register system service event handler
    let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?;
    let next_status = ServiceStatus {
        // Should match the one from system service registry
        service_type: ServiceType::OWN_PROCESS,
        // The new state
        current_state: ServiceState::Running,
        // Accept stop events when running
        controls_accepted: ServiceControlAccept::STOP,
        // Used to report an error when starting or stopping only, otherwise must be zero
        exit_code: ServiceExitCode::Win32(0),
        // Only used for pending states, otherwise must be zero
        checkpoint: 0,
        // Only used for pending states, otherwise must be zero
        wait_hint: Duration::default(),
        process_id: None,
    };

    // Tell the system that the service is running now
    status_handle.set_service_status(next_status)?;
    
    log::info!("サービスを起動しました");

    loop {

        // Poll shutdown event.
        match shutdown_rx.recv_timeout(Duration::from_secs(1)) {
            // Break the loop either upon stop or channel disconnect
            Ok(_) | Err(mpsc::RecvTimeoutError::Disconnected) => break,

            // Continue work if no events were received within the timeout
            Err(mpsc::RecvTimeoutError::Timeout) => (),
        };
    }

    // Tell the system that service has stopped.
    status_handle.set_service_status(ServiceStatus {
        service_type: ServiceType::OWN_PROCESS,
        current_state: ServiceState::Stopped,
        controls_accepted: ServiceControlAccept::empty(),
        exit_code: ServiceExitCode::Win32(0),
        checkpoint: 0,
        wait_hint: Duration::default(),
        process_id: None,
    })?;

    log::info!("サービスを終了しました");


    Ok(())
}

main.rs

サービスを実行するには「define_windows_service」で定義してあげる必要があります。
またサービス実行時のカレントディレクトリがc:\windows\system32(多分)になるので、.envファイルは実行ファイルのあるところから読み込みます

main.rs
    // 実行ファイルにある.env読み込み
    let exe_dir = std::env::current_exe().ok().and_then(|i|  i.parent().map(|z| z.to_path_buf()) ).unwrap();
    dotenvy::from_filename(exe_dir.join(".env")).ok();

    // ログ設定
    log_util::init_log()?;

    info!("プログラム開始");

    // コマンドオプションの処理(サービスのインストール、アンインストール)
    let args = Args::parse();
    if let Some(service) = args.service {
        match service {
            Service::Install => {
                let result_str = if let Ok(_) = service_install::<&str>(SERVICE_NAME, vec!["test01"]) {
                    "成功"
                } else {
                    "失敗"
                };

                log::info!("サービスのインストールに{}しました", result_str);
            }
            Service::Uninstall => {
                let result_str = if let Ok(_) = service_uninstall(SERVICE_NAME) {
                    "成功"
                } else {
                    "失敗"
                };

                log::info!("サービスのアンインストールに{}しました", result_str);
            }
        }

        return Ok(());
    }

    //サービスの開始
    service_dispatcher::start(SERVICE_NAME, ffi_service_main).unwrap();

    Ok(())

引数とログの設定

cmd_option.rs
use clap::Parser;


#[derive(Debug, Clone, clap::ValueEnum)]
pub enum Service {
    Install, Uninstall
}

/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
    #[arg(short, long, help="Windows Service Action")]
    pub service: Option<Service>,
}

log_util.rs
use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate};
use std::{env, fs};

pub fn init_log() -> anyhow::Result<()> {

    // ログファイルハンドル取得
    let log_path = std::env::current_exe()?.parent().unwrap().join("Log");
    fs::create_dir_all(&log_path)?;
    let log = FileRotate::new(
        log_path.join(env!("CARGO_PKG_NAME").to_string() + "_log"),
        AppendCount::new(10),
        ContentLimit::Time(file_rotate::TimeFrequency::Weekly),
        Compression::OnRotate(1),
        #[cfg(unix)]
        None,
    );
    
    // 環境変数RUST_LOGの値からLevelFilter取得
    let log_level: log::LevelFilter = level_filter_from_env();

    // ログ設定
    simplelog::CombinedLogger::init(vec![
        // 標準出力
        simplelog::TermLogger::new(
            log_level,
            simplelog::Config::default(),
            simplelog::TerminalMode::Mixed,
            simplelog::ColorChoice::Never,
        ),
        // ファイル出力
        simplelog::WriteLogger::new(
            log_level,
            simplelog::Config::default(),
            log,
        ),
    ])?;    

    Ok( () )
}


pub fn level_filter_from_env() -> simplelog::LevelFilter {
    let rust_log = env::var("RUST_LOG").unwrap_or_else(|_| "INFO".to_string() ).to_uppercase();

    match rust_log.as_str() {
        "DEBUG" => simplelog::LevelFilter::Debug,
        "ERROR" => simplelog::LevelFilter::Error,
        "INFO" => simplelog::LevelFilter::Info,
        "OFF" => simplelog::LevelFilter::Off,
        "TRACE" => simplelog::LevelFilter::Trace,
        "WARN" => simplelog::LevelFilter::Warn,
        _ => simplelog::LevelFilter::Info
    }
}

Discussion