空いているポート番号を探して使う

Recipe ID: net-013

ローカルサーバーを立ち上げたり、Sidecar (外部プロセス) を起動したりする際、ポート番号を固定 (30008080 など) にすると、既に他のアプリケーションで使用されていた場合に起動エラーになってしまいます。
これを防ぐために、OS に「現在空いているポート」を割り当ててもらい、その番号を取得して利用する方法を解説します。

方法 1: Rust 内でサーバーを起動する場合 (推奨)

Rust コード内で axumactix-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)
}