Open20

Tauri1.x 本番投入アプリ開発中の実践知見メモ

yunayuna

Tauriは、クロスプラットフォーム対応のデスクトップアプリ開発フレームワークです。
バージョン1.xでは、Windows, Linux, MacOS、
現在α版の2.xで、スマホアプリ対応を進めています。

Electronの代替を謳っていて、メインプロセス実装はrustを(Electronはnodeなのでjs/ts)、UI側はWEBフロントエンド技術を使った実装ができます。

2022年からWindows向けにTauri1.xを使ったアプリ開発、クライアント先に本番投入をする中で、ハマったポイントなどをメモしていきます。

※元々webアプリ開発の経験が多く、windows desktopアプリの知見が少ない、rustをある程度使える私としては、現時点でデスクトップアプリ開発には最適な選択肢でした。

▼公式サイト
https://tauri.app/

▼zennの開発環境構築〜基本はこちらの記事が参考になります
https://zenn.dev/kumassy/books/6e518fe09a86b2

▼tauri導入ステップの次に参考になったサイト
https://programwiz.org/2022/07/03/tauri-event-handlers-on-main-process-side/

▼awesome tauri(参考コードテンプレートやプラグインなどまとめ)
https://github.com/tauri-apps/awesome-tauri

実際に実装したアプリ

tauriで最初に実装したアプリは、ドキュメント管理アプリです。

PDFや画像のファイルをデスクトップから取り込んでサーバーに保管、閲覧するものです。
 デスクトップアプリにした理由は、
  ①クローズドなネットワークでの利用であること
  ②その場でスキャナからスキャンしてファイルを保存する要件

②の実現のためには、スキャナをrustからコントロールする必要があるので、tauriが最適でした。

余談ですが スキャナにはTWAIN規格という標準規格とc++などで書かれたコントローラ(https://twain.org/specification/ )があるので、全て自前で実装する場合はrustからFFIで呼ぶ方法になります。

が、それは大変なので、今回はTWAINを内部に持ちcliでスキャナを操作できる"naps2" https://www.naps2.com/ を、rustからstd::process::Commandで呼ぶ形で対応しました。

yunayuna

html5の drag & dropイベントが使えない

tauriには、ファイルのdrag & dropイベントをハンドリングできる機能が独自に備わっていて、
デフォルトでonになっている。

そのため、front(web上)のdrag & dropイベントが効かない(恐らくtauriのd&dハンドリングイベントが優先されるため)

tauri公式サイトの通り、tauri.conf.jsonファイルの以下の場所をfalseに設定しても、
なぜかこの機能が無効にならない。

↓これでもweb上のdrag & drop はまだ使えない

tauri.conf.json
{
  "tauri": {
    "windows": {
      "fileDropEnabled":false
    }
}

公式: https://tauri.app/v1/api/config/#windowconfig.filedropenabled

windowを新規で生成しているソースコード中で、
webviewのステータスを明示的に変更することで利用できるようになった。

main.rs
let builder = tauri::WindowBuilder::new(
    &app_handle,
    "print",
    windowUrl
  );
  let local_window = builder.disable_file_drop_handler().build().expect("failed the new window");

参考URL: https://issuehint.com/issue/tauri-apps/tauri/3722

yunayuna

(rust) reqwestでrustlsを使ったIdentity(client証明書)からリクエストする

※tauriではなくhttp通信crateの reqwestの話です。実装中にハマったのでメモしてます

reqwestで、featuresで"native-tls" を利用した場合、インストールされているopensslを利用してTLSリクエストを投げるが、これは環境に依存するため、問題が生じる可能性がある。

実際にOpenSSL3.0.x on ubuntuの環境で、client証明書を利用したリクエストを行うと、
本来付けるべきオプション -legacy が利用できずエラーになった。

そのため、featuresにrust実装の["rustls-tls"]を指定することで、問題回避できた。
rustls-tlsを使った場合に、気を付けないとハマるポイントがあるのでメモ。

features:["native-ssl"]を利用する場合の書き方

main.rs
let mut buf = Vec::new();
let mut reader = BufReader::new(File::open("files/client.pfx").unwrap());
let pkcs12 = reqwest::Identity::from_pkcs12_der(&buf, "password").unwrap();


let client = reqwest::Client::builder().add_root_certificate(ca_cert).identity(pkcs12).build().unwrap()

features:["rustls-ssl"]を利用する場合の書き方(環境依存しない)

事前に、pkcs12のファイルをpemに変換しておく

convert.sh
openssl pkcs12 -legacy -nodes -in files/client.pfx -out files/client.pem

clientをbuildする時にuse_rustls_tls() が必要なのがポイント。これを忘れるとNG。

main.rs
let mut buf = Vec::new();
let mut reader = BufReader::new(File::open("files/client.pem").unwrap());
reader.read_to_end(&mut buf).unwrap();
let client_pem = reqwest::Identity::from_pem(&buf).unwrap();

let client = reqwest::Client::builder().use_rustls_tls().add_root_certificate(ca_cert).identity(client_pem ).build().unwrap()

参照:
https://github.com/seanmonstar/reqwest/issues/903
https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.use_rustls_tls
https://stackoverflow.com/questions/27497723/export-a-pkcs12-file-without-an-export-password

yunayuna

アプリの複数立ち上げを禁止したい

他システムからのcallは可能?

フロントエンドとバックエンドの間でメッセージの受け渡しはできるけど、
他システムからexeを実行すると新規プロセスが起動して新windowが開いてしまう。
元windowでイベント受信する方法無いかな。

yunayuna

single instanceのplugin開発中のものを発見。
https://github.com/tauri-apps/tauri-plugin-single-instance

Linux

zbus(D-Bus用のRust crate)を使ってるらしい。

D-Busとは: D-Bus プロトコルを使用して、あるプログラムから別のプログラムに通信できる、プロセス間通信 (IPC) ソリューションの一つ。

Windows

windowsのローレベルapiを呼ぶ windows_sys::Win32 を利用。
今回はwindows用アプリ構築なので、これを真似すればできるかも
https://github.com/tauri-apps/tauri-plugin-single-instance/blob/dev/src/platform_impl/windows.rs

yunayuna

backgroundからfrontにバイナリを返したい

backgroundの関数は#[tauri::command] を付けて記述するが、返り値はserde::Serialize可能な型を満たす必要があり(たぶん)、下記のように直接バイナリを返り値をセットしたりするとコンパイルエラーになる。

公式サイト

Returned data can be of any type, as long as it implements serde::Serialize.
https://tauri.app/v1/guides/features/command/#returning-data

rust
#[tauri::command]
//※コンパイルエラー
async fn get_document_binary(app_handle: tauri::AppHandle) -> Result<bytes::Bytes, String> {

条件を満たす型を返すよう、serde_binaryなど検討したりしたが、
結局シンプルに直前にserialize(base64 encode)して、Stringで渡すのが楽だった。

rust
#[tauri::command]
async fn get_document_binary(app_handle: tauri::AppHandle) -> Result<String, String> {
  
  let binaryData : bytes::Bytes = xxxxx();
  let encoded : String = base64::encode(ret_val.as_ref());
  println!("encoded: {}",encoded );

  Ok(encoded)
}
yunayuna

ビルド、インストール後にコマンドプロンプトから該当アプリを実行すると、
なぜかjsが動作しない現象が発生。

cmd
"C:\Program Files\tauri_sample_app\tauri_sample_app.exe"

ショートカットを作成して間接的に呼ぶと問題なし。

cmd
"C:\Users\Public\Desktop\tauri_sample_app.lnk"

原因調査中。

yunayuna

Updaterの実装

公式サイトに則って、実装を進める。
https://tauri.app/v1/guides/distribution/updater/

アプリの更新チェックの際、公開鍵・秘密鍵を使った認証を行い、
更新が正しものであることを確認する仕組みがTauriに組み込まれているので、これを利用する。

鍵生成

$ npm run tauri signer -- generate -w ./updater/myapp.key

Generating new private key without password.
Please enter a password to protect the secret key.
Password: 
Password (one more time): 
Deriving a key from the password in order to encrypt the secret key... done

Your keypair was generated successfully
Private: \\?\C:\Project\sample_app\updater\myapp.key (Keep it secret!)
Public: \\?\C:\Project\sample_app\updater\myapp.key.pub
---------------------------

Environment variables used to sign:
`TAURI_PRIVATE_KEY`  Path or String of your private key
`TAURI_KEY_PASSWORD`  Your private key password (optional)

ATTENTION: If you lose your private key OR password, you'll not be able to sign your update package and updates will not work.
---------------------------

tauri.conf.jsonに、updaterの設定を記述する

pubkeyには、上記 myapp.key.pub に出力されたファイルの中身(文字列)をそのまま張り付ける。
{{target}}/{{current_version}}は、自動的に変換してくれるが、
自分で用意するendpointsに、これらの変数が不要であれば、固定で記述してOK.

tauri.conf.json
"tauri": {
    "updater": {
        "active": true,
        "endpoints": [
            "https://releases.myapp.com/{{target}}/{{current_version}}"
        ],
        "dialog": true,
        "pubkey": "YOUR_UPDATER_SIGNATURE_PUBKEY_HERE"
    }
}

endpointsのレスポンスを実装

以下のようなフォーマットで、レスポンスを返すように実装。

url:ビルドして固めたファイルの場所
version:アプリのバージョン。
このバージョンが、現在インストールされてるバージョンより新しいか否かをtauriが自動的にチェックして、更新の有無を確認してくれる。
signature:アプリをbuildしたときに生成される、xxx.sigファイルの中身の文字列をセットする

{
  "url": "https://mycompany.example.com/myapp/releases/myrelease.tar.gz",
  "version": "0.0.1",
  "notes": "Theses are some release notes",
  "pub_date": "2020-09-18T12:29:53+01:00",
  "signature": "SIGNATURE_TXT"
}

endpointsのurlに対し、このフォーマットで返せばよいので、例えば動的な実装でなくても、
s3のパスをendpointsに指定して、s3にjsonファイルを書いてアップしても良い。

tauri.conf.json
"endpoints": [
            "https://xxxx.s3.ap-northeast-1.amazonaws.com/build/release_check.json"
        ],

buildしたときに生成されたzipファイルをs3に配置し、そのパスをjson中の"url"にセット。

release_check.json
{
  "url": "https://xxxx.s3.ap-northeast-1.amazonaws.com/build/sample_app/sample_app_0.0.1_x64_ja-JP.msi.zip",
  "version": "0.0.1",
  "notes": "Theses are some release notes",
  "pub_date": "2023-01-15T21:25:53+09:00",
  "signature": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
}
yunayuna

windows用 build時に、特定のversionのwebview2(edge)を同梱する方法

※注意事項あり。最後に記述

edgeの特定バージョンでエラーが発生したため、
バグ回避のためにwebview2のバージョン更新をしたい。

webviewInstallMode をofflineInstaller にした状態のbundleから再インストールしても、edgeのバージョンが変わらないため、webviewInstallMode : fixedRuntime を試す。

Microsoft公式サイトから修正済みバージョンの.cabファイルをダウンロードし、
かつ、それを予め解凍してフォルダをこちらに展開。
cabファイルのままだとうまく動かないので注意。
src-tauri/Microsoft.WebView2.FixedVersionRuntime.110.0.1587.63.x64

https://developer.microsoft.com/ja-jp/microsoft-edge/webview2/#download-section

json
    "bundle": {
      "windows": {
        "certificateThumbprint": null,
        "digestAlgorithm": "sha256",
        "timestampUrl": "",
        "webviewInstallMode": {
          "type": "fixedRuntime",
          "path": "Microsoft.WebView2.FixedVersionRuntime.110.0.1587.63.x64"
        },

これでビルドすればOK.

cmd
$ npm run tauri build --release

※注意事項

2023/4/10時点で、上記設定にすると、develop modeの場合もこのwebviewファイルを見に行き、
なぜか見つからないエラーが出る。
build時とpathが違うのか・・・
色んな場所に上記のcab解凍フォルダを置いてみたがエラーが解消しないので、
build時だけ、webviewInstallModeをfixedRuntimeにすることで対応中。

$ npm run tauri dev

thread 'main' panicked at 'error while running tauri application: Runtime(CreateWebview(WebView2Error(WindowsError(Error { code: 0x80070002, message: 指定されたファイルが見つかりません。 }))))', src\main.rs:199:6

関連ありそうなtauri discordのthread
https://discord.com/channels/616186924390023171/731495158217965659/984185026679816262

yunayuna

bundle sizeを減らす方法

https://twitter.com/goenning/status/1635679032418222086
https://tauri.app/v1/guides/building/app-size/#stripping

コンパイルされたアプリには、関数名や変数名を含むいわゆる「デバッグシンボル」が含まれています。エンドユーザーはデバッグシンボルを気にすることはないでしょうから、この方法はバイトを節約するための確実な方法です。
最も簡単な方法は、有名なstripユーティリティを使って、このデバッグ情報を削除することです。

Rust 1.59 now has a builtin version of strip!
以下のように設定すればOK。

toml
[profile.release]
strip = true  # Automatically strip symbols from the binary.
yunayuna

別windowを開く処理

サンプル要件

  • PDFファイルが指定されたら、ローカルに保存されているPDFファイルを、webviewから表示
  • そうでなければ、普通にwebviewでデフォルトページを表示する
  • front側が発火点。新規windowを開きたいときにボタンをクリックすると、make_windowをinvokeするjsの処理を呼ぶ

front側

front.html
import { invoke } from '@tauri-apps/api/tauri'

function openNewWindow() {
    invoke('make_window',{file_name: "sss", fileSuffix: "pdf"});
}

rust側の実装

main.rs
use tauri::{Manager, WindowUrl, Window};

#[tauri::command]
async fn make_window(app_handle: tauri::AppHandle, file_name: String, file_suffix: String) -> Result<String, String> {
  
  //window開く
  let size = tauri::LogicalSize{
    width: 1200,
    height: 1000,
  };

  let mut windowUrl: WindowUrl;
  if file_suffix == "pdf" {
    //ローカルに保存されているファイルを表示する
    windowUrl = tauri::WindowUrl::App(format!("file:///{}", file_name.to_str().unwrap()).into());
  } else {
    windowUrl = tauri::WindowUrl::App("/".into());
  }
  let builder = tauri::WindowBuilder::new(
    &app_handle,
    "print",
    windowUrl
  );
  let local_window = builder.disable_file_drop_handler().build().expect("failed the new window");
  local_window.maximize();
  Ok("ok".to_string())
}

yunayuna

webviewが freeze

rust側で、あるstream処理を行ったとき、ubuntu上で動かしているときにwebviewが固まる現象が発生した。
(tauri ver1.2でも1.3でも発生)

原因は、streamのtimeoutが無限に発生し、frontのeventをcall (window.emit('xxxx')) が永久ループしていた。
当たり前だけど、frontのevent callが過剰に発生するとcpu使用率が爆上がりしてwebviewがfreezeするので注意。

yunayuna

tauri versionを1.3 -> 1.5、@tauri-apps/cli 1.1 -> 1.5.8に更新したら、
npm run tauri build --release の時にエラーが発生。

tauri \nsis\x64\installer.nsi" on line 533 -- aborting creation process

どうやらnsis関係のファイルに問題があるらしい。
https://github.com/tauri-apps/tauri/issues/8412

nsisのファイルが入ってる
~\AppData\Local\tauri のファイルを念のためまるごと削除し、
ついでにcargoのtargetやnode_moduleも全て削除して、再度ビルドしたら通った。