Open7

rust self_update crateによる自動更新処理の実装

yunayuna

windowsやlinuxへインストール型のアプリを作成した後、
運用するにあたって自動更新処理を追加する必要があるので、crateを利用する方法を含めて調べてみました。
self_update crate が良さそうなのでこれを使ってみます。
https://github.com/jaemk/self_update

※20230517追記
pythonのwebframework Flaskの作者で、sentryで仕事してる方が、新しいupdaterを作ってるみたいなのでこちらも備忘の為リンクだけ貼っておきます。
https://github.com/mitsuhiko/self-replace

※20240425追記
最新のself_updateは、内部でself-replaceを使うようになった(0.38〜)

# Changelog


## [0.38.0]
### Added
### Changed
- Use `self-replace` to replace the current executable
### Removed

https://github.com/jaemk/self_update/blob/ec8e626545e451ce88184d726a4877e779ac987d/CHANGELOG.md?plain=1#L22

yunayuna
    let updater = self_update::backends::s3::Update::configure()
        .bucket_name("sample-backet")
        .asset_prefix("self_update") //フォルダ名
        .region("us-west-2")
        .bin_name("it_agent.exe") //フォルダに保存したバイナリファイル
        .show_download_progress(true)
        .current_version(cargo_crate_version!())
        .build()?;

s3のアクセス権は、オブジェクトを取得するためのs3:GetObject、
ファイルのリストを取得するためのs3:ListBucketが必要。(※Publicにする場合は注意)

bucket: sample-backet

aws s3バケットポリシー
{
    "Version": "2012-10-17",
    "Id": "Policy1679752106008",
    "Statement": [
        {
            "Sid": "Stmt1679752104740",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "Action": [
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::sample-backet",
                "arn:aws:s3:::sample-backet/*"
            ]
        }
    ]
}
yunayuna

アップするファイル名は、ライブラリ内で指定されてるこちらのフォーマットに合わせる必要がある。

Regex::new(r"(?i)(?P<prefix>.*/)*(?P<name>.+)-[v]{0,1}(?P<version>\d+\.\d+\.\d+)-.+")

正規表現としてのポイントは、versionは "x.x.x"、その前後にハイフン"-" が入っていること。
また、バージョンのハイフンの後ろに、ターゲット名を入れる(後述)なので、
例としてはこんな感じ: "file-v1.0.1-x86_64-pc-windows-msvc.exe"

ターゲット名は、
self_updateがupdateチェック時に、該当ファイルにターゲット名が含まれるか否かをチェックししているので、ファイル中に記載が必要になる。

このターゲット名は明示的に指定しない場合、
rustの env!("TARGET") で自動的にセットされている対象プラットフォーム
(Windows上でコンパイルされた場合は "x86_64-pc-windows-msvc"、Linux上でコンパイルされた場合は "x86_64-unknown-linux-gnu" など)
となる。

yunayuna

デフォルトでは、コマンド上で更新確認が行われ、進捗状況が表示される

it_agent.exe release status:
  * Current exe: "C:\\Project\\it_agent\\target\\release\\it_agent.exe"
  * New exe release: "it_agent-0.1.3-x86_64-pc-windows-msvc.exe"
  * New exe download url: "https://sample-backet.s3.us-west-2.amazonaws.com/self_update/it_agent-0.1.3-x86_64-pc-windows-msvc.exe"

The new release will be downloaded/extracted and the existing binary will be replaced.
Do you want to continue? [Y/n] Y
Downloading...
[00:00:09] [========================================] 5.40 MiB/5.40 MiB (0s) Done                                                                                       Extracting archive... Done
Replacing binary file... Done
S3 Update status: `0.1.3`!
Update successful

確認不要の場合は、updaterを構築時、no_confirm(true)をセットすればOK。

let updater = self_update::backends::s3::Update::configure()
        .bucket_name("sample-backet")
        .asset_prefix("self_update") //フォルダ名
        .region("us-west-2")
        .bin_name("it_agent.exe") //フォルダに保存したバイナリファイル
        .show_download_progress(true)
        .current_version(cargo_crate_version!())
        .no_confirm(true)
        .build()?;

バックアップは、対象ディレクトリや名前を指定しない場合、デフォルトで以下のように保管される

parent_dir
 - it_agent.exe //binary本体
 - __it_agent.exe_backup54W3o6
   - __it_agent.exe_backup

yunayuna

この後の予定
・バックアップからバージョンを戻す処理を検証
・windows serviceを絡めた自動更新処理を検証

yunayuna

バイナリ更新後の再起動

tauriのアプリリスタート処理を参考に。
https://github.com/tauri-apps/tauri/blob/46e6187c89594692b0e245a2ad5ec50de819296b/core/tauri/src/api/process.rs#L81

pub fn restart(env: &Env) {
  use std::process::{exit, Command};

  if let Ok(path) = current_binary(env) {
    Command::new(path)
      .args(&env.args)
      .spawn()
      .expect("application failed to start");
  }

  exit(0);
}

windows serviceで実行中のアプリの場合、
net stop [service name] && net start [service name] をしたいのだが、
自分を先にストップしないといけないので、
・更新処理
・net stop/start のひとまとめの処理を、本プロセスではなく、別プロセスでやる必要があるかも

yunayuna

self_updateを使うときの注意

self_updateの中で、runtimeをblockしている箇所がありそう。
(関数(block_onなど)が現在のスレッドをブロックしようとするなど)。恐らくreqwestを使っていることが問題?)

https://github.com/jaemk/self_update/issues/44

このため、非同期処理の中でself_updateを使うとエラーが発生することがある。
今回、self_updateはアプリケーションの中で定期的に自動チェックを行うため、非同期のloop処理中で呼びたいので、対策が必要。

対策

上記github上の回避策で挙げられている通り、
tokio::task::spawn_blocking を使って別スレッド上でblocking処理を行わせることで、エラーを回避した。

#[tokio::main]
async fn main() -> Result<()> {
    tokio::task::spawn_blocking(check_for_updates).await??;

    Ok(())
}

fn check_for_updates() -> Result<Status> {
    let status = Update::configure()
        .repo_owner("ShayBox")
        .repo_name("VRC-OSC")
        .bin_name("vrc-osc")
        .show_download_progress(true)
        .current_version(CARGO_PKG_VERSION)
        .build()?
        .update()?;
    
    Ok(status)
}