リモート Mac Pro で CARTA を立ち上げてローカルの Macbook のブラウザで表示したい

研究機関を異動したら、所属先にLinuxサーバー・マシンがなくて困った！ということ、ありますよね（？）。私はあります。4月から天文学（電波天文学。アルマ望遠鏡ユーザーです）のポスドクを始めたのですが、所属研究室の望遠鏡データ解析に使える計算機が Mac Pro でした。ほとんどのデータ解析用のソフトウェアは MacOS にも対応していることが多いですが、自分で色々カスタマイズして快適に使おうとすると面倒なことが多いです。

CARTA (Cube Analysis and Rendering Tool for Astronomy) は、サイズの大きなデータ（例えばGBオーダーのFITSファイル）でも快適に表示して簡単な解析までできるソフトウェアです。とても便利です。これがないと研究できません。

# やりたいこと
- 観測データ（FITSファイル）は研究室のリモート Mac Pro の外付け HDD においてある。
- CARTA をリモート Mac Pro で起動して、表示は手元の Macbook のブラウザでやりたい。


# TL;DR
ssh でトンネルしてポート指定で CARTA を起動すれば良い。その際、Mac のデスクトップアプリを起動するのではなく、carta-backend と carta-frontend をコマンドラインで別々に起動する。以下の `carta-macpro.sh` をローカルの適当なディレクトリに、`carta_ssh.sh` をリモート Mac Pro の適当なディレクトリに置いて、ローカルで前者を走らせれば良い。

```bash:carta-macpro.sh
#!/bin/bash

REMOTE_HOST=macpro
CARTA_PORT=3002

# background ssh tunnel
if ! lsof -i tcp:${CARTA_PORT} > /dev/null 2>&1; then
  echo "Starting SSH tunnel..."
  ssh -f -N -L ${CARTA_PORT}:localhost:${CARTA_PORT} ${REMOTE_HOST}
else
  echo "Port ${CARTA_PORT} already in use. Skipping SSH tunnel."
fi

# launch CARTA on remote
LOGFILE="${HOME}/.carta/carta-remote.log"

CARTA_PID=$(ssh ${REMOTE_HOST} "/path/to/carta_ssh.sh /path/to/HOME/ ${CARTA_PORT} > '$LOGFILE' 2>&1 & echo \$!")

URL=`ssh ${REMOTE_HOST} "grep -o 'http://localhost:[0-9]*/?token=[A-Za-z0-9\-]*' ${HOME}/.carta/carta-remote.log"`
open "$URL"

while true; do
  sleep 5

  # ポート3002のWebSocket接続数を調べる（ESTABLISHEDのみ）
  CONN_COUNT=$(lsof -iTCP:3002 -sTCP:ESTABLISHED | grep -c '^')

  if [[ "$CONN_COUNT" -le 1 ]]; then
    ssh $REMOTE_HOST "kill $CARTA_PID"
    break
  fi
done
```

```bash:carta_ssh.sh
#!/bin/bash

dirname="/Applications/CARTA.app/Contents/Resources/app/carta-backend/bin"

DIR="$( cd $dirname > /dev/null && pwd )/../etc"

export CASAPATH="../../../../../$DIR linux"

$dirname/casa_data_autoupdate

$dirname/carta_backend $1 --port=$2 --frontend_folder=$dirname/../../ --no_browser ${@:3}
```

# 中身の説明
### ssh ポートフォワーディング
ssh 詳しくないですが、これしないと手元の Macbook で表示できません。ポートを指定してバックグラウンドで走らせておく。すでに走っている場合は実行しないように if で分岐させといた。
```bash
REMOTE_HOST=macpro
CARTA_PORT=3002

# background ssh tunnel
if ! lsof -i tcp:${CARTA_PORT} > /dev/null 2>&1; then
  echo "Starting SSH tunnel..."
  ssh -f -N -L ${CARTA_PORT}:localhost:${CARTA_PORT} ${REMOTE_HOST}
else
  echo "Port ${CARTA_PORT} already in use. Skipping SSH tunnel."
fi
```

### CARTA をリモートで起動
これが面倒だった。CARTA にはフロントエンドとバックエンドがあって、バックエンドで色々な計算をして、フロントエンドで html 表示させている。これらを全部ひとまとめにしたアプリ版を使えば良いのでは、とも思うが、
- ポート指定できない
- リモートで GUI が立ち上がってしまう（つまり、リモートPCがスリープ状態だと動かない。多分。）

ので、コマンドラインでフロントエンドとバックエンドを自分で立ち上げる必要がある（多分）。さらに、FITS の座標とか計算するのに必要なデータのパスをちゃんと指定してあげる必要がある。これをやったのが、
```bash:carta_ssh.sh
#!/bin/bash

dirname="/Applications/CARTA.app/Contents/Resources/app/carta-backend/bin"

DIR="$( cd $dirname > /dev/null && pwd )/../etc"

export CASAPATH="../../../../../$DIR linux"

$dirname/casa_data_autoupdate

$dirname/carta_backend $1 --port=$2 --frontend_folder=$dirname/../../ --no_browser ${@:3}
```
このシェルスクリプトをリモートで走らせた時にコンソールに表示されるアドレスにローカルでアクセスすればOK。それをやったのが以下。適当なログファイルに上のシェルスクリプトの出力を吐かせて `grep` でURLを取得、ローカルのブラウザで開いている。
```bash
# launch CARTA on remote
LOGFILE="${HOME}/.carta/carta-remote.log"

CARTA_PID=$(ssh ${REMOTE_HOST} "/path/to/carta_ssh.sh /path/to/HOME/ ${CARTA_PORT} > '$LOGFILE' 2>&1 & echo \$!")

URL=`ssh ${REMOTE_HOST} "grep -o 'http://localhost:[0-9]*/?token=[A-Za-z0-9\-]*' ${HOME}/.carta/carta-remote.log"`
open "$URL"
```

### ローカルブラウザを閉じた時にリモートの CARTA も kill する
以上のプロセスを何回もやってると CARTA のプロセスがリモートで無限に生成されることになる。そこで、指定したポート（ここでは3002）の WebSocket 接続数を定期的に監視してやることで、接続数が0になったらリモートで CARTA のプロセスを kill する。これで無限の CARTA プロセスが残ることもない。
```bash
while true; do
  sleep 5

  # ポート3002のWebSocket接続数を調べる（ESTABLISHEDのみ）
  CONN_COUNT=$(lsof -iTCP:3002 -sTCP:ESTABLISHED | grep -c '^')

  if [[ "$CONN_COUNT" -le 1 ]]; then
    ssh $REMOTE_HOST "kill $CARTA_PID"
    break
  fi
done
```

# 補足
Mac の画面共有でもいいかーと思ったが、なんか私の環境では画面共有の画面のサイズを拡大できなくて小さい画面でしか動かせなくて困った。

# 参考
ChatGPT with GPT-4o