ローカルサーバーを立ち上げたり、Sidecar (外部プロセス) を起動したりする際、ポート番号を固定 (3000 や 8080 など) にすると、既に他のアプリケーションで使用されていた場合に起動エラーになってしまいます。
これを防ぐために、OS に「現在空いているポート」を割り当ててもらい、その番号を取得して利用する方法を解説します。
方法 1: Rust 内でサーバーを起動する場合 (推奨)
Rust コード内で axum や actix-web などのサーバーを起動する場合は、バインドするアドレスのポート番号に 0 を指定します。
こうすると OS が自動的に空いているエフェメラルポートを割り当ててくれます。
割り当てられたポート番号は local_addr() メソッドで取得し、フロントエンドや他のコンポーネントに通知します。
Rust 実装例 (src-tauri/src/lib.rs)
use tauri::{AppHandle, Manager, State};
use std::net::TcpListener;
use std::sync::Mutex;
use std::time::Duration;
use tokio::time::sleep;
// ポート番号を保持するステート
struct ServerPort(Mutex<Option<u16>>);
#[tauri::command]
async fn get_server_port(state: State<'_, ServerPort>) -> Result<u16, String> {
// ポートが割り当てられるまでループで待機 (最大待ち時間: 5秒)
// ※ 本来は Notify などを使うのがスマートですが、コードを簡潔にするためポーリングで実装
let start = std::time::Instant::now();
loop {
{
let lock = state.0.lock().unwrap();
if let Some(port) = *lock {
return Ok(port);
}
}
if start.elapsed().as_secs() > 5 {
return Err("Server start timeout".to_string());
}
sleep(Duration::from_millis(100)).await;
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(ServerPort(Mutex::new(None))) // ステート初期化
.setup(|app| {
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
// ポート 0 でバインド (OSが自動割り当て)
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port");
let port = listener.local_addr().unwrap().port();
println!("Server running on port: {}", port);
// ステートに保存
if let Some(state) = app_handle.try_state::<ServerPort>() {
*state.0.lock().unwrap() = Some(port);
}
// ここで listener を使用してサーバー本体を起動
// 例: axum::serve(tokio::net::TcpListener::from_std(listener).unwrap(), router).await...
});
Ok(())
})
.invoke_handler(tauri::generate_handler![get_server_port]) // コマンド登録
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
この方法の利点は、ポート取得から使用開始までの間に、他のアプリにポートを横取りされる競合 (レースコンディション) が発生しない ことです。
また、イベントで通知する代わりにコマンドでポートを取得することで、Webview のロード完了タイミングに依存せずに確実に情報を共有できます。
フロントエンド実装例 (TypeScript)
Rust 側の get_server_port コマンドを通じてポート番号を取得します。
import { invoke } from '@tauri-apps/api/core';
async function initServer() {
try {
// コマンドを呼び出してポート番号を取得 (サーバー準備ができるまで待機してくれる)
const port = await invoke<number>('get_server_port');
const serverUrl = `http://127.0.0.1:${port}`;
console.log(`Server started on: ${serverUrl}`);
// ここで iframe の src にセットするなど
} catch (err) {
console.error("Failed to get port:", err);
}
}
---
方法 2: Sidecar 用にポートを探す場合
Python や Node.js などの Sidecar (外部プロセス) にポート番号を引数で渡したい場合、「一旦空きポートを見つけて閉じてから、Sidecar にその番号を渡す」という手順が必要になります。
注意: ポートを閉じてから Sidecar が起動するまでのわずかな間に、他のプロセスがそのポートを使ってしまう可能性(競合)はゼロではありませんが、実用上は多くのケースで問題なく動作します。
Rust 実装例 (Custom Command)
use tauri::command;
use std::net::TcpListener;
#[command]
fn get_free_port() -> Result<u16, String> {
// 一時的にバインドしてポートを取得
let listener = TcpListener::bind("127.0.0.1:0")
.map_err(|e| format!("Failed to bind: {}", e))?;
let port = listener.local_addr()
.map_err(|e| format!("Failed to get local addr: {}", e))?
.port();
// スコープを抜けると listener はドロップされ、ポートは解放される
Ok(port)
}