From 302ed7fc7df701da2f9a5bfd723dbdc0f6ce5187 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 19 Aug 2026 10:51:34 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=E8=8B=A5=E3=81=84=E7=95=AA?= =?UTF-8?q?=E5=8F=B7=E3=81=AE=20tmux=20=E3=82=BB=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=81=B8=E6=88=BB=E3=81=99=20tmux-first=20?= =?UTF-8?q?=E3=82=92=20base=20=E3=82=A4=E3=83=A1=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=81=B8=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VS Code のウィンドウが異常終了 (クラッシュ / ホスト再起動 / 接続断) すると、 VS Code サーバー側に pty が取り残され、tmux クライアントだけがセッションへ 繋がったまま残る。統合ターミナルの起動スクリプトはそのセッションを「使用中」と 判定するため、-1 に戻れず -2, -3 ... と新しいセッションが 増え続ける。 tmux-first は対象セッションに居座っている自分以外のクライアントを切断し、 一番若い番号のセッションへ現在の端末を切り替える。ベース名は現在のセッション名 から末尾の -数字 を除いて推定し、対象は数字の昇順で最小のものを選ぶ。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017BpznFY4zi8kvY7hhHV7EK --- CHANGELOG.md | 7 ++ containers/base/Dockerfile | 154 +++++++++++++++++++++++++++++ docs/user/environment-variables.md | 18 ++++ 3 files changed, 179 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4144120..615d2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ ## [Unreleased] ### Added +- **`tmux-first` コマンドを base イメージへ追加**しました。VS Code のウィンドウが + 異常終了 (クラッシュ / ホスト再起動 / 接続断) すると、VS Code サーバー側に pty が + 取り残され、tmux クライアントだけがセッションへ繋がったまま残ります。統合ターミナルの + 起動スクリプトはそのセッションを「使用中」と判定するため、`-1` に戻れず + `-2`, `-3` ... と新しいセッションが増え続けます。`tmux-first` は居座って + いるクライアントを切断し、一番若い番号のセッションへ現在の端末を切り替えます。 + `tmux-first -n` で切り替えずに対象を確認でき、`tmux1` のエイリアスも用意しています。 - **bi-tools プロジェクト用コンテナ (`containers/bi-tools`)** を追加しました。 `devbase-base` に dbt-core + dbt-bigquery(1.12 系 / Python 3.11)と Lightdash CLI (`@lightdash/cli`) を追加し、BigQuery 上の共有 dbt プロジェクト(`dbt build` / `test`)と diff --git a/containers/base/Dockerfile b/containers/base/Dockerfile index 99b1070..8ad1432 100644 --- a/containers/base/Dockerfile +++ b/containers/base/Dockerfile @@ -169,6 +169,160 @@ RUN set -eux; \ mkdir -p ~/.claude; \ echo '{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"echo '\''unset AWS_REGION'\'' >> \"$CLAUDE_ENV_FILE\""}]}]}}' > ~/.claude/settings.json; +# tmux-first: 若い番号の tmux セッションへ戻すコマンド +# +# VS Code のウィンドウが異常終了 (クラッシュ / ホスト再起動 / 接続断) すると、 +# VS Code サーバー側に pty が取り残され、tmux クライアントだけがセッションに +# 繋がったまま残る。統合ターミナルの起動スクリプトは「クライアントが繋がって +# いるセッション = 使用中」と判定するため、-1 に戻れず -2、 +# -3 ... と新しいセッションを作り続けてしまう。tmux-first はその居座り +# クライアントを切断し、一番若い番号のセッションへ現在の端末を切り替える。 +# 詳細は docs/user/environment-variables.md を参照。 +COPY --chmod=0755 <<'TMUX_FIRST_SH' /usr/local/bin/tmux-first +#!/bin/sh +# tmux-first - 同じベース名のセッションのうち、最も番号の小さいものへ切り替える。 +# +# VS Code のウィンドウが異常終了 (クラッシュ / ホスト再起動 / 接続断) すると、 +# VS Code サーバー側に pty が取り残され、tmux クライアントだけがセッションに +# 繋がったまま残る。次に開いた端末はそのセッションを「使用中」と判断するため、 +# adminer-1 に戻らず adminer-2, adminer-3 ... と新しいセッションを作ってしまう。 +# +# このコマンドは、対象セッションに居座っている自分以外のクライアントを切断し、 +# 一番若い番号のセッションへ現在の端末を切り替える。 +# +# tmux-first 現在のセッション名からベース名を推定 (adminer-2 -> adminer) +# tmux-first adminer ベース名を明示 +# tmux-first -n 切り替えず、対象と切断予定のクライアントだけ表示 +# +set -eu + +DRY=0 +BASE="" +usage() { + cat <<'USAGE' +使い方: tmux-first [-n|--dry-run] [ベース名] + + 引数なし 現在の tmux セッション名から末尾の -数字 を除いたものをベース名にする + (tmux の外にいる場合はカレントディレクトリ名) + ベース名 明示指定 (例: tmux-first adminer) + -n 切り替えを行わず、対象セッションと切断予定のクライアントを表示する +USAGE +} + +for a in "$@"; do + case "$a" in + -n | --dry-run) DRY=1 ;; + -h | --help) + usage + exit 0 + ;; + -*) + echo "tmux-first: 不明なオプション: $a" >&2 + usage >&2 + exit 2 + ;; + *) BASE="$a" ;; + esac +done + +command -v tmux >/dev/null 2>&1 || { + echo "tmux-first: tmux が見つかりません" >&2 + exit 1 +} +tmux list-sessions >/dev/null 2>&1 || { + echo "tmux-first: tmux サーバーが起動していません" >&2 + exit 1 +} + +# ベース名: 引数 > 現在のセッション名から末尾の -数字 を除去 > カレントディレクトリ名 +if [ -z "$BASE" ]; then + if [ -n "${TMUX:-}" ]; then + BASE=$(tmux display-message -p '#S' | sed -E 's/-[0-9]+$//') + else + BASE=$(basename "$PWD" | tr './:' '___') + fi +fi +[ -n "$BASE" ] || { + echo "tmux-first: ベース名を決定できません" >&2 + exit 1 +} + +# <ベース名>-<数字> のセッションを集め、数字の昇順で先頭を採る。 +# ベース名に '-' や正規表現メタ文字が含まれても壊れないよう awk で前方一致比較する。 +TARGET=$(tmux list-sessions -F '#{session_name}' | awk -v b="$BASE" ' + { + n = length(b) + if (substr($0, 1, n + 1) == b "-") { + suf = substr($0, n + 2) + if (suf ~ /^[0-9]+$/) printf "%d\t%s\n", suf, $0 + } + }' | sort -n | head -1 | cut -f2-) + +if [ -z "$TARGET" ]; then + echo "tmux-first: '${BASE}-<数字>' に一致するセッションがありません" >&2 + echo " 既存のセッション:" >&2 + tmux list-sessions -F ' - #{session_name}' >&2 + exit 1 +fi + +# 自分自身のクライアント (これは絶対に切断しない) +ME="" +if [ -n "${TMUX:-}" ]; then + ME=$(tmux display-message -p '#{client_name}' 2>/dev/null || true) +fi + +# 対象セッションに繋がっている自分以外のクライアント = 居座り。 +# tmux の中にいるのに自分のクライアントを特定できない場合は、自分自身を切断して +# しまう恐れがあるため切断対象を空にする (切り替えだけ行う)。 +GHOSTS="" +if [ -n "${TMUX:-}" ] && [ -z "$ME" ]; then + echo "tmux-first: 自分のクライアントを特定できないため切断は行いません" >&2 +else + GHOSTS=$(tmux list-clients -t "=$TARGET" -F '#{client_name}' 2>/dev/null | + grep -v -x -F "${ME:-__no_client__}" || true) +fi + +if [ "$DRY" = 1 ]; then + echo "base : $BASE" + echo "target : $TARGET" + if [ -n "$ME" ]; then + echo "self : $ME" + elif [ -n "${TMUX:-}" ]; then + echo "self : (クライアント未接続)" + else + echo "self : (tmux の外)" + fi + if [ -n "$GHOSTS" ]; then + echo "$GHOSTS" | sed 's/^/detach : /' + else + echo "detach : (なし)" + fi + exit 0 +fi + +if [ -n "$GHOSTS" ]; then + echo "$GHOSTS" | while IFS= read -r c; do + [ -n "$c" ] || continue + echo "tmux-first: 居座りクライアントを切断: $c -> $TARGET" + tmux detach-client -t "$c" || true + done +fi + +if [ -n "${TMUX:-}" ]; then + if [ "$(tmux display-message -p '#S')" = "$TARGET" ]; then + echo "tmux-first: すでに $TARGET です" + else + echo "tmux-first: $TARGET へ切り替えます" + tmux switch-client -t "=$TARGET" + fi +else + echo "tmux-first: $TARGET にアタッチします" + exec tmux attach-session -t "=$TARGET" +fi +TMUX_FIRST_SH + +RUN echo "alias tmux1='tmux-first'" >> ~/.bashrc + # entrypoint と dind COPY --chmod=755 entrypoint.sh /entrypoint.sh COPY --chmod=755 dind /usr/local/bin/dind diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index ca6eb76..1202a60 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -227,6 +227,24 @@ fi 反映するには `tmux kill-server` でサーバーを作り直してください(`~/.tmux.conf` はサーバー起動時にのみ読まれ、既存ペインのシェルも修正前の rc で起動しているため)。 +##### 同じプロジェクトのセッションが増え続ける場合 + +VS Code のウィンドウが異常終了すると、VS Code サーバー側に pty が取り残され、tmux クライアントだけがセッションへ繋がったまま残ります。統合ターミナルの起動スクリプトは「クライアントが繋がっているセッション = 使用中」と判定するため、`adminer-1` に戻れず `adminer-2`, `adminer-3` … と新しいセッションが増えていきます。 + +このときは dev コンテナに同梱の `tmux-first` を実行してください。居座っているクライアントを切断したうえで、一番若い番号のセッションへ現在の端末を切り替えます。 + +```bash +tmux-first # 現在のセッション名からベース名を推定 (adminer-2 -> adminer) +tmux-first -n # 切り替えず、対象と切断予定のクライアントだけ表示する +tmux1 # tmux-first のエイリアス +``` + +誰がいつから繋いでいるかは次で確認できます。 + +```bash +tmux list-clients -F '#{client_name} -> #{client_session} 最終操作 #{t:client_activity}' +``` + ## ソースファイル変更検出 devbase はソースファイル(`~/.aws/config` 等)のハッシュを `.env.sources.yml` で管理しています。 From 9a01e653b3dc4a6f8dc8b56c4e5a7f6879c92794 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 19 Aug 2026 11:17:54 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=E7=BD=AE=E3=81=8D=E5=8E=BB?= =?UTF-8?q?=E3=82=8A=E3=82=BB=E3=83=83=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92?= =?UTF-8?q?=E6=8E=83=E9=99=A4=E3=81=99=E3=82=8B=20tmux-clean=20=E3=82=92?= =?UTF-8?q?=20base=20=E3=82=A4=E3=83=A1=E3=83=BC=E3=82=B8=E3=81=B8?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tmux-first で若い番号のセッションへ戻したあと、置き去りになった -<数字> のセッションを削除するコマンドを追加する。 既定では安全側に倒し、keeper (tmux 内なら現在のセッション、外なら最小 番号)、アタッチ中のセッション、シェル以外を実行中のセッションは残す。 -n で削除対象と除外理由を確認でき、-f ですべて削除する。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017BpznFY4zi8kvY7hhHV7EK --- CHANGELOG.md | 5 + containers/base/Dockerfile | 171 ++++++++++++++++++++++++++++- docs/user/environment-variables.md | 9 ++ 3 files changed, 182 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 615d2d1..5efb566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ `-2`, `-3` ... と新しいセッションが増え続けます。`tmux-first` は居座って いるクライアントを切断し、一番若い番号のセッションへ現在の端末を切り替えます。 `tmux-first -n` で切り替えずに対象を確認でき、`tmux1` のエイリアスも用意しています。 +- **`tmux-clean` コマンドを base イメージへ追加**しました。`tmux-first` で若い番号の + セッションへ戻したあと、置き去りになった `-<数字>` のセッションを削除します。 + 既定では安全側に倒し、keeper (tmux 内なら現在のセッション、外なら最小番号) と、 + アタッチ中のセッション、シェル以外を実行中のセッションは残します。`-n` で削除対象と + 除外理由を確認でき、`-f` ですべて削除します。`tmuxc` のエイリアスも用意しています。 - **bi-tools プロジェクト用コンテナ (`containers/bi-tools`)** を追加しました。 `devbase-base` に dbt-core + dbt-bigquery(1.12 系 / Python 3.11)と Lightdash CLI (`@lightdash/cli`) を追加し、BigQuery 上の共有 dbt プロジェクト(`dbt build` / `test`)と diff --git a/containers/base/Dockerfile b/containers/base/Dockerfile index 8ad1432..77043bd 100644 --- a/containers/base/Dockerfile +++ b/containers/base/Dockerfile @@ -169,7 +169,7 @@ RUN set -eux; \ mkdir -p ~/.claude; \ echo '{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"echo '\''unset AWS_REGION'\'' >> \"$CLAUDE_ENV_FILE\""}]}]}}' > ~/.claude/settings.json; -# tmux-first: 若い番号の tmux セッションへ戻すコマンド +# tmux-first / tmux-clean: tmux セッションの整理コマンド # # VS Code のウィンドウが異常終了 (クラッシュ / ホスト再起動 / 接続断) すると、 # VS Code サーバー側に pty が取り残され、tmux クライアントだけがセッションに @@ -177,7 +177,8 @@ RUN set -eux; \ # いるセッション = 使用中」と判定するため、-1 に戻れず -2、 # -3 ... と新しいセッションを作り続けてしまう。tmux-first はその居座り # クライアントを切断し、一番若い番号のセッションへ現在の端末を切り替える。 -# 詳細は docs/user/environment-variables.md を参照。 +# tmux-clean は置き去りになったセッションを削除する (既定では未アタッチかつ +# シェル以外を実行していないもののみ)。詳細は docs/user/environment-variables.md を参照。 COPY --chmod=0755 <<'TMUX_FIRST_SH' /usr/local/bin/tmux-first #!/bin/sh # tmux-first - 同じベース名のセッションのうち、最も番号の小さいものへ切り替える。 @@ -321,7 +322,171 @@ else fi TMUX_FIRST_SH -RUN echo "alias tmux1='tmux-first'" >> ~/.bashrc +COPY --chmod=0755 <<'TMUX_CLEAN_SH' /usr/local/bin/tmux-clean +#!/bin/sh +# tmux-clean - 同じベース名の不要な tmux セッションを掃除する。 +# +# tmux-first で若い番号のセッションへ戻したあと、置き去りになった <ベース名>-<数字> +# のセッションを削除するためのコマンド。既定では安全側に倒し、次のセッションは +# 残す: +# +# - 残すセッション (keeper): tmux 内なら現在のセッション、外なら最小番号のもの +# - アタッチ中のセッション : 誰かが使用中とみなす +# - シェル以外が動いているセッション: ビルドやサーバを実行中とみなす +# +# 上記も含めて消したい場合は -f を付ける。 +# +# tmux-clean 現在のセッション名からベース名を推定して掃除 +# tmux-clean adminer ベース名を明示 +# tmux-clean -n 削除せず、対象と除外理由だけ表示 (dry-run) +# tmux-clean -f アタッチ中・実行中のセッションも削除する +# +set -eu + +DRY=0 +FORCE=0 +BASE="" + +usage() { + cat <<'USAGE' +使い方: tmux-clean [-n|--dry-run] [-f|--force] [ベース名] + + 引数なし 現在の tmux セッション名から末尾の -数字 を除いたものをベース名にする + (tmux の外にいる場合はカレントディレクトリ名) + ベース名 明示指定 (例: tmux-clean adminer) + -n 削除せず、削除対象と除外理由を表示する + -f アタッチ中・シェル以外を実行中のセッションも削除する + +残すセッション (keeper) は tmux 内なら現在のセッション、tmux 外なら最小番号のもの。 +USAGE +} + +for a in "$@"; do + case "$a" in + -n | --dry-run) DRY=1 ;; + -f | --force) FORCE=1 ;; + -h | --help) + usage + exit 0 + ;; + -*) + echo "tmux-clean: 不明なオプション: $a" >&2 + usage >&2 + exit 2 + ;; + *) BASE="$a" ;; + esac +done + +command -v tmux >/dev/null 2>&1 || { + echo "tmux-clean: tmux が見つかりません" >&2 + exit 1 +} +tmux list-sessions >/dev/null 2>&1 || { + echo "tmux-clean: tmux サーバーが起動していません" >&2 + exit 1 +} + +# ベース名: 引数 > 現在のセッション名から末尾の -数字 を除去 > カレントディレクトリ名 +if [ -z "$BASE" ]; then + if [ -n "${TMUX:-}" ]; then + BASE=$(tmux display-message -p '#S' | sed -E 's/-[0-9]+$//') + else + BASE=$(basename "$PWD" | tr './:' '___') + fi +fi +[ -n "$BASE" ] || { + echo "tmux-clean: ベース名を決定できません" >&2 + exit 1 +} + +# <ベース名>-<数字> のセッションを数字の昇順で列挙する。 +# ベース名に '-' や正規表現メタ文字が含まれても壊れないよう awk で前方一致比較する。 +SESSIONS=$(tmux list-sessions -F '#{session_name}' | awk -v b="$BASE" ' + { + n = length(b) + if (substr($0, 1, n + 1) == b "-") { + suf = substr($0, n + 2) + if (suf ~ /^[0-9]+$/) printf "%d\t%s\n", suf, $0 + } + }' | sort -n | cut -f2-) + +if [ -z "$SESSIONS" ]; then + echo "tmux-clean: '${BASE}-<数字>' に一致するセッションがありません" >&2 + exit 1 +fi + +# keeper: tmux 内なら現在のセッション、外なら最小番号。 +# 現在のセッションがベース名に属さない場合も最小番号を残す。 +KEEP="" +if [ -n "${TMUX:-}" ]; then + CUR=$(tmux display-message -p '#S' 2>/dev/null || true) + for s in $SESSIONS; do + [ "$s" = "$CUR" ] && KEEP="$s" && break + done +fi +[ -n "$KEEP" ] || KEEP=$(printf '%s\n' "$SESSIONS" | head -1) +echo "tmux-clean: base=$BASE keeper=$KEEP" + +# シェルとみなすコマンド名 (これしか動いていなければ削除して良いと判断する) +is_shell() { + case "$1" in + sh | bash | zsh | dash | fish | ksh | tcsh | csh | ash | -sh | -bash | -zsh | login) return 0 ;; + *) return 1 ;; + esac +} + +KILLED=0 +KEPT=0 +for s in $SESSIONS; do + if [ "$s" = "$KEEP" ]; then + echo " keep $s (keeper)" + KEPT=$((KEPT + 1)) + continue + fi + + # display-message の -t は target-pane を取るためセッション指定に使えない。 + # また tmux は書式内のタブを '_' に置換するため区切り文字での parse も不可。 + # list-sessions のフィルタで名前完全一致のものだけを取り出す。 + ATT=$(tmux list-sessions -F '#{session_attached}' \ + -f "#{==:#{session_name},$s}" 2>/dev/null) + BUSY="" + for c in $(tmux list-panes -s -t "=$s" -F '#{pane_current_command}' 2>/dev/null); do + if ! is_shell "$c"; then + BUSY="$c" + break + fi + done + + if [ "$FORCE" = 0 ] && [ "${ATT:-0}" != 0 ]; then + echo " keep $s (アタッチ中: クライアント ${ATT} 個)" + KEPT=$((KEPT + 1)) + continue + fi + if [ "$FORCE" = 0 ] && [ -n "$BUSY" ]; then + echo " keep $s (実行中: $BUSY)" + KEPT=$((KEPT + 1)) + continue + fi + + if [ "$DRY" = 1 ]; then + echo " KILL $s (dry-run)" + else + tmux kill-session -t "=$s" + echo " KILL $s" + fi + KILLED=$((KILLED + 1)) +done + +if [ "$DRY" = 1 ]; then + echo "tmux-clean: 削除対象 ${KILLED} 件 / 残す ${KEPT} 件 (dry-run のため実行していません)" +else + echo "tmux-clean: 削除 ${KILLED} 件 / 残す ${KEPT} 件" +fi +TMUX_CLEAN_SH + +RUN echo "alias tmux1='tmux-first'" >> ~/.bashrc \ + && echo "alias tmuxc='tmux-clean'" >> ~/.bashrc # entrypoint と dind COPY --chmod=755 entrypoint.sh /entrypoint.sh diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index 1202a60..ec74919 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -245,6 +245,15 @@ tmux1 # tmux-first のエイリアス tmux list-clients -F '#{client_name} -> #{client_session} 最終操作 #{t:client_activity}' ``` +切り替わったあと、そのセッションのプロンプトで `tmux-clean` を実行すると置き去りのセッションを削除できます。安全側の既定として、keeper(tmux 内なら現在のセッション、tmux 外なら最小番号)、アタッチ中のセッション、シェル以外を実行中のセッションは残します。 + +```bash +tmux-clean # 不要なセッションを削除 +tmux-clean -n # 削除せず、削除対象と除外理由を表示する +tmux-clean -f # アタッチ中・実行中のセッションも削除する +tmuxc # tmux-clean のエイリアス +``` + ## ソースファイル変更検出 devbase はソースファイル(`~/.aws/config` 等)のハッシュを `.env.sources.yml` で管理しています。 From 131951645c6c1cce4da6559a1265c466f19dad45 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 19 Aug 2026 15:59:22 +0900 Subject: [PATCH 3/8] =?UTF-8?q?refactor:=20tmux-first=20/=20tmux-clean=20?= =?UTF-8?q?=E3=82=92=20Dockerfile=20=E3=81=AE=E3=83=92=E3=82=A2=E3=83=89?= =?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=E3=81=8B=E3=82=89?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=B8?= =?UTF-8?q?=E5=88=87=E3=82=8A=E5=87=BA=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dockerfile 内にシェルスクリプトをベタ書きすると差分が読みづらく、 シンタックスハイライトや shellcheck も効かないため、 containers/base/tmux-first, tmux-clean として切り出して COPY する (entrypoint.sh / dind と同じ方式)。スクリプト本文に変更はない。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M3p9U34HsArSpxJ1E1SK94 --- containers/base/Dockerfile | 320 +------------------------------------ containers/base/tmux-clean | 160 +++++++++++++++++++ containers/base/tmux-first | 140 ++++++++++++++++ 3 files changed, 305 insertions(+), 315 deletions(-) create mode 100644 containers/base/tmux-clean create mode 100644 containers/base/tmux-first diff --git a/containers/base/Dockerfile b/containers/base/Dockerfile index 77043bd..d242dbe 100644 --- a/containers/base/Dockerfile +++ b/containers/base/Dockerfile @@ -169,321 +169,11 @@ RUN set -eux; \ mkdir -p ~/.claude; \ echo '{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"echo '\''unset AWS_REGION'\'' >> \"$CLAUDE_ENV_FILE\""}]}]}}' > ~/.claude/settings.json; -# tmux-first / tmux-clean: tmux セッションの整理コマンド -# -# VS Code のウィンドウが異常終了 (クラッシュ / ホスト再起動 / 接続断) すると、 -# VS Code サーバー側に pty が取り残され、tmux クライアントだけがセッションに -# 繋がったまま残る。統合ターミナルの起動スクリプトは「クライアントが繋がって -# いるセッション = 使用中」と判定するため、-1 に戻れず -2、 -# -3 ... と新しいセッションを作り続けてしまう。tmux-first はその居座り -# クライアントを切断し、一番若い番号のセッションへ現在の端末を切り替える。 -# tmux-clean は置き去りになったセッションを削除する (既定では未アタッチかつ -# シェル以外を実行していないもののみ)。詳細は docs/user/environment-variables.md を参照。 -COPY --chmod=0755 <<'TMUX_FIRST_SH' /usr/local/bin/tmux-first -#!/bin/sh -# tmux-first - 同じベース名のセッションのうち、最も番号の小さいものへ切り替える。 -# -# VS Code のウィンドウが異常終了 (クラッシュ / ホスト再起動 / 接続断) すると、 -# VS Code サーバー側に pty が取り残され、tmux クライアントだけがセッションに -# 繋がったまま残る。次に開いた端末はそのセッションを「使用中」と判断するため、 -# adminer-1 に戻らず adminer-2, adminer-3 ... と新しいセッションを作ってしまう。 -# -# このコマンドは、対象セッションに居座っている自分以外のクライアントを切断し、 -# 一番若い番号のセッションへ現在の端末を切り替える。 -# -# tmux-first 現在のセッション名からベース名を推定 (adminer-2 -> adminer) -# tmux-first adminer ベース名を明示 -# tmux-first -n 切り替えず、対象と切断予定のクライアントだけ表示 -# -set -eu - -DRY=0 -BASE="" -usage() { - cat <<'USAGE' -使い方: tmux-first [-n|--dry-run] [ベース名] - - 引数なし 現在の tmux セッション名から末尾の -数字 を除いたものをベース名にする - (tmux の外にいる場合はカレントディレクトリ名) - ベース名 明示指定 (例: tmux-first adminer) - -n 切り替えを行わず、対象セッションと切断予定のクライアントを表示する -USAGE -} - -for a in "$@"; do - case "$a" in - -n | --dry-run) DRY=1 ;; - -h | --help) - usage - exit 0 - ;; - -*) - echo "tmux-first: 不明なオプション: $a" >&2 - usage >&2 - exit 2 - ;; - *) BASE="$a" ;; - esac -done - -command -v tmux >/dev/null 2>&1 || { - echo "tmux-first: tmux が見つかりません" >&2 - exit 1 -} -tmux list-sessions >/dev/null 2>&1 || { - echo "tmux-first: tmux サーバーが起動していません" >&2 - exit 1 -} - -# ベース名: 引数 > 現在のセッション名から末尾の -数字 を除去 > カレントディレクトリ名 -if [ -z "$BASE" ]; then - if [ -n "${TMUX:-}" ]; then - BASE=$(tmux display-message -p '#S' | sed -E 's/-[0-9]+$//') - else - BASE=$(basename "$PWD" | tr './:' '___') - fi -fi -[ -n "$BASE" ] || { - echo "tmux-first: ベース名を決定できません" >&2 - exit 1 -} - -# <ベース名>-<数字> のセッションを集め、数字の昇順で先頭を採る。 -# ベース名に '-' や正規表現メタ文字が含まれても壊れないよう awk で前方一致比較する。 -TARGET=$(tmux list-sessions -F '#{session_name}' | awk -v b="$BASE" ' - { - n = length(b) - if (substr($0, 1, n + 1) == b "-") { - suf = substr($0, n + 2) - if (suf ~ /^[0-9]+$/) printf "%d\t%s\n", suf, $0 - } - }' | sort -n | head -1 | cut -f2-) - -if [ -z "$TARGET" ]; then - echo "tmux-first: '${BASE}-<数字>' に一致するセッションがありません" >&2 - echo " 既存のセッション:" >&2 - tmux list-sessions -F ' - #{session_name}' >&2 - exit 1 -fi - -# 自分自身のクライアント (これは絶対に切断しない) -ME="" -if [ -n "${TMUX:-}" ]; then - ME=$(tmux display-message -p '#{client_name}' 2>/dev/null || true) -fi - -# 対象セッションに繋がっている自分以外のクライアント = 居座り。 -# tmux の中にいるのに自分のクライアントを特定できない場合は、自分自身を切断して -# しまう恐れがあるため切断対象を空にする (切り替えだけ行う)。 -GHOSTS="" -if [ -n "${TMUX:-}" ] && [ -z "$ME" ]; then - echo "tmux-first: 自分のクライアントを特定できないため切断は行いません" >&2 -else - GHOSTS=$(tmux list-clients -t "=$TARGET" -F '#{client_name}' 2>/dev/null | - grep -v -x -F "${ME:-__no_client__}" || true) -fi - -if [ "$DRY" = 1 ]; then - echo "base : $BASE" - echo "target : $TARGET" - if [ -n "$ME" ]; then - echo "self : $ME" - elif [ -n "${TMUX:-}" ]; then - echo "self : (クライアント未接続)" - else - echo "self : (tmux の外)" - fi - if [ -n "$GHOSTS" ]; then - echo "$GHOSTS" | sed 's/^/detach : /' - else - echo "detach : (なし)" - fi - exit 0 -fi - -if [ -n "$GHOSTS" ]; then - echo "$GHOSTS" | while IFS= read -r c; do - [ -n "$c" ] || continue - echo "tmux-first: 居座りクライアントを切断: $c -> $TARGET" - tmux detach-client -t "$c" || true - done -fi - -if [ -n "${TMUX:-}" ]; then - if [ "$(tmux display-message -p '#S')" = "$TARGET" ]; then - echo "tmux-first: すでに $TARGET です" - else - echo "tmux-first: $TARGET へ切り替えます" - tmux switch-client -t "=$TARGET" - fi -else - echo "tmux-first: $TARGET にアタッチします" - exec tmux attach-session -t "=$TARGET" -fi -TMUX_FIRST_SH - -COPY --chmod=0755 <<'TMUX_CLEAN_SH' /usr/local/bin/tmux-clean -#!/bin/sh -# tmux-clean - 同じベース名の不要な tmux セッションを掃除する。 -# -# tmux-first で若い番号のセッションへ戻したあと、置き去りになった <ベース名>-<数字> -# のセッションを削除するためのコマンド。既定では安全側に倒し、次のセッションは -# 残す: -# -# - 残すセッション (keeper): tmux 内なら現在のセッション、外なら最小番号のもの -# - アタッチ中のセッション : 誰かが使用中とみなす -# - シェル以外が動いているセッション: ビルドやサーバを実行中とみなす -# -# 上記も含めて消したい場合は -f を付ける。 -# -# tmux-clean 現在のセッション名からベース名を推定して掃除 -# tmux-clean adminer ベース名を明示 -# tmux-clean -n 削除せず、対象と除外理由だけ表示 (dry-run) -# tmux-clean -f アタッチ中・実行中のセッションも削除する -# -set -eu - -DRY=0 -FORCE=0 -BASE="" - -usage() { - cat <<'USAGE' -使い方: tmux-clean [-n|--dry-run] [-f|--force] [ベース名] - - 引数なし 現在の tmux セッション名から末尾の -数字 を除いたものをベース名にする - (tmux の外にいる場合はカレントディレクトリ名) - ベース名 明示指定 (例: tmux-clean adminer) - -n 削除せず、削除対象と除外理由を表示する - -f アタッチ中・シェル以外を実行中のセッションも削除する - -残すセッション (keeper) は tmux 内なら現在のセッション、tmux 外なら最小番号のもの。 -USAGE -} - -for a in "$@"; do - case "$a" in - -n | --dry-run) DRY=1 ;; - -f | --force) FORCE=1 ;; - -h | --help) - usage - exit 0 - ;; - -*) - echo "tmux-clean: 不明なオプション: $a" >&2 - usage >&2 - exit 2 - ;; - *) BASE="$a" ;; - esac -done - -command -v tmux >/dev/null 2>&1 || { - echo "tmux-clean: tmux が見つかりません" >&2 - exit 1 -} -tmux list-sessions >/dev/null 2>&1 || { - echo "tmux-clean: tmux サーバーが起動していません" >&2 - exit 1 -} - -# ベース名: 引数 > 現在のセッション名から末尾の -数字 を除去 > カレントディレクトリ名 -if [ -z "$BASE" ]; then - if [ -n "${TMUX:-}" ]; then - BASE=$(tmux display-message -p '#S' | sed -E 's/-[0-9]+$//') - else - BASE=$(basename "$PWD" | tr './:' '___') - fi -fi -[ -n "$BASE" ] || { - echo "tmux-clean: ベース名を決定できません" >&2 - exit 1 -} - -# <ベース名>-<数字> のセッションを数字の昇順で列挙する。 -# ベース名に '-' や正規表現メタ文字が含まれても壊れないよう awk で前方一致比較する。 -SESSIONS=$(tmux list-sessions -F '#{session_name}' | awk -v b="$BASE" ' - { - n = length(b) - if (substr($0, 1, n + 1) == b "-") { - suf = substr($0, n + 2) - if (suf ~ /^[0-9]+$/) printf "%d\t%s\n", suf, $0 - } - }' | sort -n | cut -f2-) - -if [ -z "$SESSIONS" ]; then - echo "tmux-clean: '${BASE}-<数字>' に一致するセッションがありません" >&2 - exit 1 -fi - -# keeper: tmux 内なら現在のセッション、外なら最小番号。 -# 現在のセッションがベース名に属さない場合も最小番号を残す。 -KEEP="" -if [ -n "${TMUX:-}" ]; then - CUR=$(tmux display-message -p '#S' 2>/dev/null || true) - for s in $SESSIONS; do - [ "$s" = "$CUR" ] && KEEP="$s" && break - done -fi -[ -n "$KEEP" ] || KEEP=$(printf '%s\n' "$SESSIONS" | head -1) -echo "tmux-clean: base=$BASE keeper=$KEEP" - -# シェルとみなすコマンド名 (これしか動いていなければ削除して良いと判断する) -is_shell() { - case "$1" in - sh | bash | zsh | dash | fish | ksh | tcsh | csh | ash | -sh | -bash | -zsh | login) return 0 ;; - *) return 1 ;; - esac -} - -KILLED=0 -KEPT=0 -for s in $SESSIONS; do - if [ "$s" = "$KEEP" ]; then - echo " keep $s (keeper)" - KEPT=$((KEPT + 1)) - continue - fi - - # display-message の -t は target-pane を取るためセッション指定に使えない。 - # また tmux は書式内のタブを '_' に置換するため区切り文字での parse も不可。 - # list-sessions のフィルタで名前完全一致のものだけを取り出す。 - ATT=$(tmux list-sessions -F '#{session_attached}' \ - -f "#{==:#{session_name},$s}" 2>/dev/null) - BUSY="" - for c in $(tmux list-panes -s -t "=$s" -F '#{pane_current_command}' 2>/dev/null); do - if ! is_shell "$c"; then - BUSY="$c" - break - fi - done - - if [ "$FORCE" = 0 ] && [ "${ATT:-0}" != 0 ]; then - echo " keep $s (アタッチ中: クライアント ${ATT} 個)" - KEPT=$((KEPT + 1)) - continue - fi - if [ "$FORCE" = 0 ] && [ -n "$BUSY" ]; then - echo " keep $s (実行中: $BUSY)" - KEPT=$((KEPT + 1)) - continue - fi - - if [ "$DRY" = 1 ]; then - echo " KILL $s (dry-run)" - else - tmux kill-session -t "=$s" - echo " KILL $s" - fi - KILLED=$((KILLED + 1)) -done - -if [ "$DRY" = 1 ]; then - echo "tmux-clean: 削除対象 ${KILLED} 件 / 残す ${KEPT} 件 (dry-run のため実行していません)" -else - echo "tmux-clean: 削除 ${KILLED} 件 / 残す ${KEPT} 件" -fi -TMUX_CLEAN_SH +# tmux セッションの整理コマンド。tmux-first は居座りクライアントを切断して +# 一番若い番号のセッションへ切り替え、tmux-clean は置き去りのセッションを削除する。 +# 背景と使い方は各スクリプト冒頭のコメントと docs/user/environment-variables.md を参照。 +COPY --chmod=0755 tmux-first /usr/local/bin/tmux-first +COPY --chmod=0755 tmux-clean /usr/local/bin/tmux-clean RUN echo "alias tmux1='tmux-first'" >> ~/.bashrc \ && echo "alias tmuxc='tmux-clean'" >> ~/.bashrc diff --git a/containers/base/tmux-clean b/containers/base/tmux-clean new file mode 100644 index 0000000..5cf1269 --- /dev/null +++ b/containers/base/tmux-clean @@ -0,0 +1,160 @@ +#!/bin/sh +# tmux-clean - 同じベース名の不要な tmux セッションを掃除する。 +# +# tmux-first で若い番号のセッションへ戻したあと、置き去りになった <ベース名>-<数字> +# のセッションを削除するためのコマンド。既定では安全側に倒し、次のセッションは +# 残す: +# +# - 残すセッション (keeper): tmux 内なら現在のセッション、外なら最小番号のもの +# - アタッチ中のセッション : 誰かが使用中とみなす +# - シェル以外が動いているセッション: ビルドやサーバを実行中とみなす +# +# 上記も含めて消したい場合は -f を付ける。 +# +# tmux-clean 現在のセッション名からベース名を推定して掃除 +# tmux-clean adminer ベース名を明示 +# tmux-clean -n 削除せず、対象と除外理由だけ表示 (dry-run) +# tmux-clean -f アタッチ中・実行中のセッションも削除する +# +set -eu + +DRY=0 +FORCE=0 +BASE="" + +usage() { + cat <<'USAGE' +使い方: tmux-clean [-n|--dry-run] [-f|--force] [ベース名] + + 引数なし 現在の tmux セッション名から末尾の -数字 を除いたものをベース名にする + (tmux の外にいる場合はカレントディレクトリ名) + ベース名 明示指定 (例: tmux-clean adminer) + -n 削除せず、削除対象と除外理由を表示する + -f アタッチ中・シェル以外を実行中のセッションも削除する + +残すセッション (keeper) は tmux 内なら現在のセッション、tmux 外なら最小番号のもの。 +USAGE +} + +for a in "$@"; do + case "$a" in + -n | --dry-run) DRY=1 ;; + -f | --force) FORCE=1 ;; + -h | --help) + usage + exit 0 + ;; + -*) + echo "tmux-clean: 不明なオプション: $a" >&2 + usage >&2 + exit 2 + ;; + *) BASE="$a" ;; + esac +done + +command -v tmux >/dev/null 2>&1 || { + echo "tmux-clean: tmux が見つかりません" >&2 + exit 1 +} +tmux list-sessions >/dev/null 2>&1 || { + echo "tmux-clean: tmux サーバーが起動していません" >&2 + exit 1 +} + +# ベース名: 引数 > 現在のセッション名から末尾の -数字 を除去 > カレントディレクトリ名 +if [ -z "$BASE" ]; then + if [ -n "${TMUX:-}" ]; then + BASE=$(tmux display-message -p '#S' | sed -E 's/-[0-9]+$//') + else + BASE=$(basename "$PWD" | tr './:' '___') + fi +fi +[ -n "$BASE" ] || { + echo "tmux-clean: ベース名を決定できません" >&2 + exit 1 +} + +# <ベース名>-<数字> のセッションを数字の昇順で列挙する。 +# ベース名に '-' や正規表現メタ文字が含まれても壊れないよう awk で前方一致比較する。 +SESSIONS=$(tmux list-sessions -F '#{session_name}' | awk -v b="$BASE" ' + { + n = length(b) + if (substr($0, 1, n + 1) == b "-") { + suf = substr($0, n + 2) + if (suf ~ /^[0-9]+$/) printf "%d\t%s\n", suf, $0 + } + }' | sort -n | cut -f2-) + +if [ -z "$SESSIONS" ]; then + echo "tmux-clean: '${BASE}-<数字>' に一致するセッションがありません" >&2 + exit 1 +fi + +# keeper: tmux 内なら現在のセッション、外なら最小番号。 +# 現在のセッションがベース名に属さない場合も最小番号を残す。 +KEEP="" +if [ -n "${TMUX:-}" ]; then + CUR=$(tmux display-message -p '#S' 2>/dev/null || true) + for s in $SESSIONS; do + [ "$s" = "$CUR" ] && KEEP="$s" && break + done +fi +[ -n "$KEEP" ] || KEEP=$(printf '%s\n' "$SESSIONS" | head -1) +echo "tmux-clean: base=$BASE keeper=$KEEP" + +# シェルとみなすコマンド名 (これしか動いていなければ削除して良いと判断する) +is_shell() { + case "$1" in + sh | bash | zsh | dash | fish | ksh | tcsh | csh | ash | -sh | -bash | -zsh | login) return 0 ;; + *) return 1 ;; + esac +} + +KILLED=0 +KEPT=0 +for s in $SESSIONS; do + if [ "$s" = "$KEEP" ]; then + echo " keep $s (keeper)" + KEPT=$((KEPT + 1)) + continue + fi + + # display-message の -t は target-pane を取るためセッション指定に使えない。 + # また tmux は書式内のタブを '_' に置換するため区切り文字での parse も不可。 + # list-sessions のフィルタで名前完全一致のものだけを取り出す。 + ATT=$(tmux list-sessions -F '#{session_attached}' \ + -f "#{==:#{session_name},$s}" 2>/dev/null) + BUSY="" + for c in $(tmux list-panes -s -t "=$s" -F '#{pane_current_command}' 2>/dev/null); do + if ! is_shell "$c"; then + BUSY="$c" + break + fi + done + + if [ "$FORCE" = 0 ] && [ "${ATT:-0}" != 0 ]; then + echo " keep $s (アタッチ中: クライアント ${ATT} 個)" + KEPT=$((KEPT + 1)) + continue + fi + if [ "$FORCE" = 0 ] && [ -n "$BUSY" ]; then + echo " keep $s (実行中: $BUSY)" + KEPT=$((KEPT + 1)) + continue + fi + + if [ "$DRY" = 1 ]; then + echo " KILL $s (dry-run)" + else + tmux kill-session -t "=$s" + echo " KILL $s" + fi + KILLED=$((KILLED + 1)) +done + +if [ "$DRY" = 1 ]; then + echo "tmux-clean: 削除対象 ${KILLED} 件 / 残す ${KEPT} 件 (dry-run のため実行していません)" +else + echo "tmux-clean: 削除 ${KILLED} 件 / 残す ${KEPT} 件" +fi diff --git a/containers/base/tmux-first b/containers/base/tmux-first new file mode 100644 index 0000000..ca9e451 --- /dev/null +++ b/containers/base/tmux-first @@ -0,0 +1,140 @@ +#!/bin/sh +# tmux-first - 同じベース名のセッションのうち、最も番号の小さいものへ切り替える。 +# +# VS Code のウィンドウが異常終了 (クラッシュ / ホスト再起動 / 接続断) すると、 +# VS Code サーバー側に pty が取り残され、tmux クライアントだけがセッションに +# 繋がったまま残る。次に開いた端末はそのセッションを「使用中」と判断するため、 +# adminer-1 に戻らず adminer-2, adminer-3 ... と新しいセッションを作ってしまう。 +# +# このコマンドは、対象セッションに居座っている自分以外のクライアントを切断し、 +# 一番若い番号のセッションへ現在の端末を切り替える。 +# +# tmux-first 現在のセッション名からベース名を推定 (adminer-2 -> adminer) +# tmux-first adminer ベース名を明示 +# tmux-first -n 切り替えず、対象と切断予定のクライアントだけ表示 +# +set -eu + +DRY=0 +BASE="" +usage() { + cat <<'USAGE' +使い方: tmux-first [-n|--dry-run] [ベース名] + + 引数なし 現在の tmux セッション名から末尾の -数字 を除いたものをベース名にする + (tmux の外にいる場合はカレントディレクトリ名) + ベース名 明示指定 (例: tmux-first adminer) + -n 切り替えを行わず、対象セッションと切断予定のクライアントを表示する +USAGE +} + +for a in "$@"; do + case "$a" in + -n | --dry-run) DRY=1 ;; + -h | --help) + usage + exit 0 + ;; + -*) + echo "tmux-first: 不明なオプション: $a" >&2 + usage >&2 + exit 2 + ;; + *) BASE="$a" ;; + esac +done + +command -v tmux >/dev/null 2>&1 || { + echo "tmux-first: tmux が見つかりません" >&2 + exit 1 +} +tmux list-sessions >/dev/null 2>&1 || { + echo "tmux-first: tmux サーバーが起動していません" >&2 + exit 1 +} + +# ベース名: 引数 > 現在のセッション名から末尾の -数字 を除去 > カレントディレクトリ名 +if [ -z "$BASE" ]; then + if [ -n "${TMUX:-}" ]; then + BASE=$(tmux display-message -p '#S' | sed -E 's/-[0-9]+$//') + else + BASE=$(basename "$PWD" | tr './:' '___') + fi +fi +[ -n "$BASE" ] || { + echo "tmux-first: ベース名を決定できません" >&2 + exit 1 +} + +# <ベース名>-<数字> のセッションを集め、数字の昇順で先頭を採る。 +# ベース名に '-' や正規表現メタ文字が含まれても壊れないよう awk で前方一致比較する。 +TARGET=$(tmux list-sessions -F '#{session_name}' | awk -v b="$BASE" ' + { + n = length(b) + if (substr($0, 1, n + 1) == b "-") { + suf = substr($0, n + 2) + if (suf ~ /^[0-9]+$/) printf "%d\t%s\n", suf, $0 + } + }' | sort -n | head -1 | cut -f2-) + +if [ -z "$TARGET" ]; then + echo "tmux-first: '${BASE}-<数字>' に一致するセッションがありません" >&2 + echo " 既存のセッション:" >&2 + tmux list-sessions -F ' - #{session_name}' >&2 + exit 1 +fi + +# 自分自身のクライアント (これは絶対に切断しない) +ME="" +if [ -n "${TMUX:-}" ]; then + ME=$(tmux display-message -p '#{client_name}' 2>/dev/null || true) +fi + +# 対象セッションに繋がっている自分以外のクライアント = 居座り。 +# tmux の中にいるのに自分のクライアントを特定できない場合は、自分自身を切断して +# しまう恐れがあるため切断対象を空にする (切り替えだけ行う)。 +GHOSTS="" +if [ -n "${TMUX:-}" ] && [ -z "$ME" ]; then + echo "tmux-first: 自分のクライアントを特定できないため切断は行いません" >&2 +else + GHOSTS=$(tmux list-clients -t "=$TARGET" -F '#{client_name}' 2>/dev/null | + grep -v -x -F "${ME:-__no_client__}" || true) +fi + +if [ "$DRY" = 1 ]; then + echo "base : $BASE" + echo "target : $TARGET" + if [ -n "$ME" ]; then + echo "self : $ME" + elif [ -n "${TMUX:-}" ]; then + echo "self : (クライアント未接続)" + else + echo "self : (tmux の外)" + fi + if [ -n "$GHOSTS" ]; then + echo "$GHOSTS" | sed 's/^/detach : /' + else + echo "detach : (なし)" + fi + exit 0 +fi + +if [ -n "$GHOSTS" ]; then + echo "$GHOSTS" | while IFS= read -r c; do + [ -n "$c" ] || continue + echo "tmux-first: 居座りクライアントを切断: $c -> $TARGET" + tmux detach-client -t "$c" || true + done +fi + +if [ -n "${TMUX:-}" ]; then + if [ "$(tmux display-message -p '#S')" = "$TARGET" ]; then + echo "tmux-first: すでに $TARGET です" + else + echo "tmux-first: $TARGET へ切り替えます" + tmux switch-client -t "=$TARGET" + fi +else + echo "tmux-first: $TARGET にアタッチします" + exec tmux attach-session -t "=$TARGET" +fi From 57965b9ebb95f595f2dfda658221565e57de2745 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 19 Aug 2026 16:11:54 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20tmux-first/tmux-clean=20=E3=81=AE?= =?UTF-8?q?=E6=97=A2=E5=AE=9A=E5=8B=95=E4=BD=9C=E3=81=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=B8=AD=E3=81=AE=E3=82=AF=E3=83=A9=E3=82=A4=E3=82=A2=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=83=BB=E3=82=B8=E3=83=A7=E3=83=96=E3=82=92=E5=B7=BB?= =?UTF-8?q?=E3=81=8D=E6=B7=BB=E3=81=88=E3=81=AB=E3=81=97=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=82=88=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit レビュー指摘 (codex round 1) への対応。 - tmux-first: 対象セッションに接続しているというだけで切断せず、最終操作から TMUX_FIRST_IDLE 秒 (既定 300) 以上経過したクライアントだけを居座りとみなす。 操作中のクライアントは残し、-f を付けたときのみ全件切断する。 dry-run では切断対象に加えて残すクライアントと経過秒数も表示する。 - tmux-clean: pane_current_command は foreground しか見えないため、 pane のシェル (pane_pid) に子プロセスが残っていれば実行中とみなして残す。 /proc//task/*/children を優先し、無い環境では pgrep -P にフォールバック。 - CHANGELOG / docs へ上記の既定動作を反映。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M3p9U34HsArSpxJ1E1SK94 --- CHANGELOG.md | 13 ++++--- containers/base/tmux-clean | 34 ++++++++++++++++--- containers/base/tmux-first | 54 +++++++++++++++++++++++++++--- docs/user/environment-variables.md | 8 +++-- 4 files changed, 92 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5efb566..4f02527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,14 +9,17 @@ 異常終了 (クラッシュ / ホスト再起動 / 接続断) すると、VS Code サーバー側に pty が 取り残され、tmux クライアントだけがセッションへ繋がったまま残ります。統合ターミナルの 起動スクリプトはそのセッションを「使用中」と判定するため、`-1` に戻れず - `-2`, `-3` ... と新しいセッションが増え続けます。`tmux-first` は居座って - いるクライアントを切断し、一番若い番号のセッションへ現在の端末を切り替えます。 - `tmux-first -n` で切り替えずに対象を確認でき、`tmux1` のエイリアスも用意しています。 + `-2`, `-3` ... と新しいセッションが増え続けます。`tmux-first` は放置された + クライアントを切断し、一番若い番号のセッションへ現在の端末を切り替えます。切断対象は + 最終操作から `TMUX_FIRST_IDLE` 秒 (既定 300) 以上経過したクライアントだけで、使用中の + 端末は残します (`-f` で全件切断)。`tmux-first -n` で切り替えずに対象を確認でき、 + `tmux1` のエイリアスも用意しています。 - **`tmux-clean` コマンドを base イメージへ追加**しました。`tmux-first` で若い番号の セッションへ戻したあと、置き去りになった `-<数字>` のセッションを削除します。 既定では安全側に倒し、keeper (tmux 内なら現在のセッション、外なら最小番号) と、 - アタッチ中のセッション、シェル以外を実行中のセッションは残します。`-n` で削除対象と - 除外理由を確認でき、`-f` ですべて削除します。`tmuxc` のエイリアスも用意しています。 + アタッチ中のセッション、シェル以外を実行中のセッション、`&` で起動したバックグラウンド + ジョブが残っているセッションは残します。`-n` で削除対象と除外理由を確認でき、`-f` で + すべて削除します。`tmuxc` のエイリアスも用意しています。 - **bi-tools プロジェクト用コンテナ (`containers/bi-tools`)** を追加しました。 `devbase-base` に dbt-core + dbt-bigquery(1.12 系 / Python 3.11)と Lightdash CLI (`@lightdash/cli`) を追加し、BigQuery 上の共有 dbt プロジェクト(`dbt build` / `test`)と diff --git a/containers/base/tmux-clean b/containers/base/tmux-clean index 5cf1269..7fb993e 100644 --- a/containers/base/tmux-clean +++ b/containers/base/tmux-clean @@ -8,13 +8,14 @@ # - 残すセッション (keeper): tmux 内なら現在のセッション、外なら最小番号のもの # - アタッチ中のセッション : 誰かが使用中とみなす # - シェル以外が動いているセッション: ビルドやサーバを実行中とみなす +# - シェルに子プロセスが残っているセッション: & で起動したジョブが動いているとみなす # # 上記も含めて消したい場合は -f を付ける。 # # tmux-clean 現在のセッション名からベース名を推定して掃除 # tmux-clean adminer ベース名を明示 # tmux-clean -n 削除せず、対象と除外理由だけ表示 (dry-run) -# tmux-clean -f アタッチ中・実行中のセッションも削除する +# tmux-clean -f アタッチ中・実行中 (バックグラウンドジョブ含む) も削除する # set -eu @@ -30,7 +31,7 @@ usage() { (tmux の外にいる場合はカレントディレクトリ名) ベース名 明示指定 (例: tmux-clean adminer) -n 削除せず、削除対象と除外理由を表示する - -f アタッチ中・シェル以外を実行中のセッションも削除する + -f アタッチ中・実行中 (バックグラウンドジョブ含む) のセッションも削除する 残すセッション (keeper) は tmux 内なら現在のセッション、tmux 外なら最小番号のもの。 USAGE @@ -111,6 +112,21 @@ is_shell() { esac } +# pane_current_command は foreground のコマンドしか示さないため、シェルから +# `&` や nohup で起動したサーバー・ジョブがあっても「シェルだけ」に見えてしまう。 +# pane のシェル (pane_pid) に子プロセスが残っていれば実行中とみなして残す。 +has_child_process() { + for f in /proc/"$1"/task/*/children; do + [ -r "$f" ] || continue + [ -n "$(cat "$f" 2>/dev/null || true)" ] && return 0 + done + # /proc//task/*/children が使えない環境向けのフォールバック + if command -v pgrep >/dev/null 2>&1; then + pgrep -P "$1" >/dev/null 2>&1 && return 0 + fi + return 1 +} + KILLED=0 KEPT=0 for s in $SESSIONS; do @@ -126,9 +142,17 @@ for s in $SESSIONS; do ATT=$(tmux list-sessions -F '#{session_attached}' \ -f "#{==:#{session_name},$s}" 2>/dev/null) BUSY="" - for c in $(tmux list-panes -s -t "=$s" -F '#{pane_current_command}' 2>/dev/null); do - if ! is_shell "$c"; then - BUSY="$c" + # pane_pid も pane_current_command も空白を含まないため ':' で連結して分解する。 + for e in $(tmux list-panes -s -t "=$s" \ + -F '#{pane_pid}:#{pane_current_command}' 2>/dev/null); do + pid=${e%%:*} + cmd=${e#*:} + if ! is_shell "$cmd"; then + BUSY="$cmd" + break + fi + if [ -n "$pid" ] && has_child_process "$pid"; then + BUSY="バックグラウンドジョブ (pane pid $pid)" break fi done diff --git a/containers/base/tmux-first b/containers/base/tmux-first index ca9e451..29d163e 100644 --- a/containers/base/tmux-first +++ b/containers/base/tmux-first @@ -6,31 +6,46 @@ # 繋がったまま残る。次に開いた端末はそのセッションを「使用中」と判断するため、 # adminer-1 に戻らず adminer-2, adminer-3 ... と新しいセッションを作ってしまう。 # -# このコマンドは、対象セッションに居座っている自分以外のクライアントを切断し、 +# このコマンドは、対象セッションに居座っている「放置されたクライアント」を切断し、 # 一番若い番号のセッションへ現在の端末を切り替える。 # +# 接続しているというだけでは居座りとは判定しない。最終操作からの経過時間が +# TMUX_FIRST_IDLE 秒 (既定 300) 以上のクライアントだけを切断対象とし、 +# 現に使われている端末は残す。すべて切断したい場合は -f を明示する。 +# # tmux-first 現在のセッション名からベース名を推定 (adminer-2 -> adminer) # tmux-first adminer ベース名を明示 # tmux-first -n 切り替えず、対象と切断予定のクライアントだけ表示 +# tmux-first -f 操作中のクライアントも含めて切断する # set -eu DRY=0 +FORCE=0 BASE="" +NL=' +' +# 最終操作からこの秒数以上経過したクライアントを「放置」とみなす。 +IDLE_MIN=${TMUX_FIRST_IDLE:-300} usage() { cat <<'USAGE' -使い方: tmux-first [-n|--dry-run] [ベース名] +使い方: tmux-first [-n|--dry-run] [-f|--force] [ベース名] 引数なし 現在の tmux セッション名から末尾の -数字 を除いたものをベース名にする (tmux の外にいる場合はカレントディレクトリ名) ベース名 明示指定 (例: tmux-first adminer) -n 切り替えを行わず、対象セッションと切断予定のクライアントを表示する + -f 操作中のクライアントも含めて切断する + +既定では、最終操作から TMUX_FIRST_IDLE 秒 (既定 300) 以上経過したクライアントだけを +切断する。使用中の端末を巻き添えにしないための安全策。 USAGE } for a in "$@"; do case "$a" in -n | --dry-run) DRY=1 ;; + -f | --force) FORCE=1 ;; -h | --help) usage exit 0 @@ -90,15 +105,41 @@ if [ -n "${TMUX:-}" ]; then ME=$(tmux display-message -p '#{client_name}' 2>/dev/null || true) fi -# 対象セッションに繋がっている自分以外のクライアント = 居座り。 +# 対象セッションに繋がっている自分以外のクライアントのうち、最終操作から +# IDLE_MIN 秒以上経過したものだけを「居座り」とみなす。接続しているだけの +# クライアントを切断すると、使用中の別ウィンドウや別の利用者の端末まで +# 巻き添えにしてしまうため、既定では操作中のものは残す (-f で全件切断)。 +# # tmux の中にいるのに自分のクライアントを特定できない場合は、自分自身を切断して # しまう恐れがあるため切断対象を空にする (切り替えだけ行う)。 GHOSTS="" +ALIVE="" if [ -n "${TMUX:-}" ] && [ -z "$ME" ]; then echo "tmux-first: 自分のクライアントを特定できないため切断は行いません" >&2 else - GHOSTS=$(tmux list-clients -t "=$TARGET" -F '#{client_name}' 2>/dev/null | - grep -v -x -F "${ME:-__no_client__}" || true) + NOW=$(date +%s) + # client_name も client_activity も空白を含まないため ':' で連結して分解する。 + for e in $(tmux list-clients -t "=$TARGET" \ + -F '#{client_activity}:#{client_name}' 2>/dev/null); do + act=${e%%:*} + name=${e#*:} + [ -n "$name" ] || continue + [ "$name" = "${ME:-__no_client__}" ] && continue + case "$act" in + '' | *[!0-9]*) act=0 ;; + esac + idle=$((NOW - act)) + if [ "$FORCE" = 1 ] || [ "$idle" -ge "$IDLE_MIN" ]; then + GHOSTS="${GHOSTS:+${GHOSTS}${NL}}${name}" + else + ALIVE="${ALIVE:+${ALIVE}${NL}}${name} (最終操作 ${idle} 秒前)" + fi + done +fi + +if [ -n "$ALIVE" ]; then + echo "tmux-first: 操作中のクライアントは切断しません (切断するには -f):" >&2 + echo "$ALIVE" | sed 's/^/ - /' >&2 fi if [ "$DRY" = 1 ]; then @@ -116,6 +157,9 @@ if [ "$DRY" = 1 ]; then else echo "detach : (なし)" fi + if [ -n "$ALIVE" ]; then + echo "$ALIVE" | sed 's/^/keep : /' + fi exit 0 fi diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index ec74919..6d7b652 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -231,11 +231,15 @@ fi VS Code のウィンドウが異常終了すると、VS Code サーバー側に pty が取り残され、tmux クライアントだけがセッションへ繋がったまま残ります。統合ターミナルの起動スクリプトは「クライアントが繋がっているセッション = 使用中」と判定するため、`adminer-1` に戻れず `adminer-2`, `adminer-3` … と新しいセッションが増えていきます。 -このときは dev コンテナに同梱の `tmux-first` を実行してください。居座っているクライアントを切断したうえで、一番若い番号のセッションへ現在の端末を切り替えます。 +このときは dev コンテナに同梱の `tmux-first` を実行してください。放置されたクライアントを切断したうえで、一番若い番号のセッションへ現在の端末を切り替えます。 + +接続しているというだけでは居座りとは判定しません。切断対象は最終操作から `TMUX_FIRST_IDLE` 秒(既定 300)以上経過したクライアントだけで、いま使われている別ウィンドウの端末は残します(`-f` で全件切断)。 ```bash tmux-first # 現在のセッション名からベース名を推定 (adminer-2 -> adminer) tmux-first -n # 切り替えず、対象と切断予定のクライアントだけ表示する +tmux-first -f # 操作中のクライアントも含めて切断する +TMUX_FIRST_IDLE=60 tmux-first # 放置とみなす秒数を変える tmux1 # tmux-first のエイリアス ``` @@ -245,7 +249,7 @@ tmux1 # tmux-first のエイリアス tmux list-clients -F '#{client_name} -> #{client_session} 最終操作 #{t:client_activity}' ``` -切り替わったあと、そのセッションのプロンプトで `tmux-clean` を実行すると置き去りのセッションを削除できます。安全側の既定として、keeper(tmux 内なら現在のセッション、tmux 外なら最小番号)、アタッチ中のセッション、シェル以外を実行中のセッションは残します。 +切り替わったあと、そのセッションのプロンプトで `tmux-clean` を実行すると置き去りのセッションを削除できます。安全側の既定として、keeper(tmux 内なら現在のセッション、tmux 外なら最小番号)、アタッチ中のセッション、シェル以外を実行中のセッション、`&` で起動したバックグラウンドジョブが残っているセッションは残します。 ```bash tmux-clean # 不要なセッションを削除 From 83b1d73c0aa66bcf45d9e50478435a98267ca227 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 19 Aug 2026 16:21:35 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20TMUX=5FFIRST=5FIDLE=20=E3=81=AE?= =?UTF-8?q?=E4=B8=8D=E6=AD=A3=E5=80=A4=E3=82=92=E5=BC=BE=E3=81=8D=E3=80=81?= =?UTF-8?q?tmux-clean=20=E3=81=AE=20kill-session=20=E7=AB=B6=E5=90=88?= =?UTF-8?q?=E3=81=A7=E4=B8=AD=E6=96=AD=E3=81=97=E3=81=AA=E3=81=84=E3=82=88?= =?UTF-8?q?=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tmux-first: TMUX_FIRST_IDLE に負数や数値以外が入ると全クライアントが idle 条件を 満たし、-f なしで操作中の端末まで切断されるため、引数解釈後に 0 以上の整数か検証し 不正値では exit 2 で終了する - tmux-clean: kill-session の直前にセッションが終了していた場合の競合エラーで set -e により後続の削除処理が止まらないよう || true を付与 - CHANGELOG / docs/user/environment-variables.md に TMUX_FIRST_IDLE の制約を追記 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M3p9U34HsArSpxJ1E1SK94 --- CHANGELOG.md | 4 ++-- containers/base/tmux-clean | 4 +++- containers/base/tmux-first | 11 +++++++++++ docs/user/environment-variables.md | 2 ++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f02527..b7bf17e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ 起動スクリプトはそのセッションを「使用中」と判定するため、`-1` に戻れず `-2`, `-3` ... と新しいセッションが増え続けます。`tmux-first` は放置された クライアントを切断し、一番若い番号のセッションへ現在の端末を切り替えます。切断対象は - 最終操作から `TMUX_FIRST_IDLE` 秒 (既定 300) 以上経過したクライアントだけで、使用中の - 端末は残します (`-f` で全件切断)。`tmux-first -n` で切り替えずに対象を確認でき、 + 最終操作から `TMUX_FIRST_IDLE` 秒 (既定 300、0 以上の整数のみ有効) 以上経過した + クライアントだけで、使用中の端末は残します (`-f` で全件切断)。`tmux-first -n` で切り替えずに対象を確認でき、 `tmux1` のエイリアスも用意しています。 - **`tmux-clean` コマンドを base イメージへ追加**しました。`tmux-first` で若い番号の セッションへ戻したあと、置き去りになった `-<数字>` のセッションを削除します。 diff --git a/containers/base/tmux-clean b/containers/base/tmux-clean index 7fb993e..ead403a 100644 --- a/containers/base/tmux-clean +++ b/containers/base/tmux-clean @@ -171,7 +171,9 @@ for s in $SESSIONS; do if [ "$DRY" = 1 ]; then echo " KILL $s (dry-run)" else - tmux kill-session -t "=$s" + # 直前にセッションが終了していた場合の競合エラーで set -e により + # 後続の削除処理が止まらないようにする。 + tmux kill-session -t "=$s" || true echo " KILL $s" fi KILLED=$((KILLED + 1)) diff --git a/containers/base/tmux-first b/containers/base/tmux-first index 29d163e..eae4981 100644 --- a/containers/base/tmux-first +++ b/containers/base/tmux-first @@ -12,6 +12,7 @@ # 接続しているというだけでは居座りとは判定しない。最終操作からの経過時間が # TMUX_FIRST_IDLE 秒 (既定 300) 以上のクライアントだけを切断対象とし、 # 現に使われている端末は残す。すべて切断したい場合は -f を明示する。 +# TMUX_FIRST_IDLE は 0 以上の整数のみ有効で、それ以外はエラー終了する。 # # tmux-first 現在のセッション名からベース名を推定 (adminer-2 -> adminer) # tmux-first adminer ベース名を明示 @@ -26,6 +27,8 @@ BASE="" NL=' ' # 最終操作からこの秒数以上経過したクライアントを「放置」とみなす。 +# 負数や数値以外を許すとすべてのクライアントが idle 条件を満たしてしまい、-f なしで +# 操作中の端末まで切断されるため、0 以上の整数以外は受け付けない。 IDLE_MIN=${TMUX_FIRST_IDLE:-300} usage() { cat <<'USAGE' @@ -39,6 +42,7 @@ usage() { 既定では、最終操作から TMUX_FIRST_IDLE 秒 (既定 300) 以上経過したクライアントだけを 切断する。使用中の端末を巻き添えにしないための安全策。 +TMUX_FIRST_IDLE に 0 以上の整数以外 (負数・数値以外) を指定した場合はエラー終了する。 USAGE } @@ -59,6 +63,13 @@ for a in "$@"; do esac done +case "$IDLE_MIN" in +'' | *[!0-9]*) + echo "tmux-first: TMUX_FIRST_IDLE は 0 以上の整数で指定してください: ${TMUX_FIRST_IDLE:-}" >&2 + exit 2 + ;; +esac + command -v tmux >/dev/null 2>&1 || { echo "tmux-first: tmux が見つかりません" >&2 exit 1 diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index 6d7b652..b26ca59 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -235,6 +235,8 @@ VS Code のウィンドウが異常終了すると、VS Code サーバー側に 接続しているというだけでは居座りとは判定しません。切断対象は最終操作から `TMUX_FIRST_IDLE` 秒(既定 300)以上経過したクライアントだけで、いま使われている別ウィンドウの端末は残します(`-f` で全件切断)。 +`TMUX_FIRST_IDLE` に指定できるのは 0 以上の整数だけです。負数や数値以外を指定すると、すべてのクライアントが放置扱いになってしまうため `tmux-first` はエラー終了します。 + ```bash tmux-first # 現在のセッション名からベース名を推定 (adminer-2 -> adminer) tmux-first -n # 切り替えず、対象と切断予定のクライアントだけ表示する From 732ed2da36d0e0ec0183cd1515832714efa46e83 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 19 Aug 2026 16:36:51 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20TMUX=5FFIRST=5FIDLE=20=E3=81=AE?= =?UTF-8?q?=E7=A9=BA=E6=96=87=E5=AD=97=E3=82=92=E5=BC=BE=E3=81=8D=E3=80=81?= =?UTF-8?q?kill=20=E5=A4=B1=E6=95=97=E3=81=AE=E6=8F=A1=E3=82=8A=E3=81=A4?= =?UTF-8?q?=E3=81=B6=E3=81=97=E3=81=A8=20alias=20=E4=BE=9D=E5=AD=98?= =?UTF-8?q?=E3=82=92=E8=A7=A3=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tmux-first: `${TMUX_FIRST_IDLE:-300}` を `${TMUX_FIRST_IDLE-300}` に変更。 明示的な空文字が既定値 300 に化けて検証を素通りしていたため、 「0 以上の整数以外はエラー終了」というドキュメントの契約と食い違っていた。 - tmux-clean: `kill-session || true` が競合以外の失敗も成功として KILL 表示・ KILLED 加算していた。削除成功時のみ加算し、対象消失は skip、それ以外の失敗は 標準エラーへ出力して失敗件数と終了ステータス 1 で通知する。 - Dockerfile: tmux1 / tmuxc を ~/.bashrc の alias ではなく /usr/local/bin の symlink で提供する。alias は bash の対話シェルにしか効かず、zsh や docker exec などの非対話実行では使えないため。 - CHANGELOG / docs も上記に合わせて更新。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M3p9U34HsArSpxJ1E1SK94 --- CHANGELOG.md | 7 ++++--- containers/base/Dockerfile | 6 ++++-- containers/base/tmux-clean | 21 +++++++++++++++------ containers/base/tmux-first | 3 ++- docs/user/environment-variables.md | 6 +++--- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7bf17e..9f41cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,15 +11,16 @@ 起動スクリプトはそのセッションを「使用中」と判定するため、`-1` に戻れず `-2`, `-3` ... と新しいセッションが増え続けます。`tmux-first` は放置された クライアントを切断し、一番若い番号のセッションへ現在の端末を切り替えます。切断対象は - 最終操作から `TMUX_FIRST_IDLE` 秒 (既定 300、0 以上の整数のみ有効) 以上経過した + 最終操作から `TMUX_FIRST_IDLE` 秒 (未設定時のみ既定 300、0 以上の整数のみ有効) 以上経過した クライアントだけで、使用中の端末は残します (`-f` で全件切断)。`tmux-first -n` で切り替えずに対象を確認でき、 - `tmux1` のエイリアスも用意しています。 + `tmux1` の短縮コマンド (symlink) も用意しています。 - **`tmux-clean` コマンドを base イメージへ追加**しました。`tmux-first` で若い番号の セッションへ戻したあと、置き去りになった `-<数字>` のセッションを削除します。 既定では安全側に倒し、keeper (tmux 内なら現在のセッション、外なら最小番号) と、 アタッチ中のセッション、シェル以外を実行中のセッション、`&` で起動したバックグラウンド ジョブが残っているセッションは残します。`-n` で削除対象と除外理由を確認でき、`-f` で - すべて削除します。`tmuxc` のエイリアスも用意しています。 + すべて削除します。削除に失敗したセッションがあれば標準エラーへ出力し、終了ステータス 1 で + 知らせます。`tmuxc` の短縮コマンド (symlink) も用意しています。 - **bi-tools プロジェクト用コンテナ (`containers/bi-tools`)** を追加しました。 `devbase-base` に dbt-core + dbt-bigquery(1.12 系 / Python 3.11)と Lightdash CLI (`@lightdash/cli`) を追加し、BigQuery 上の共有 dbt プロジェクト(`dbt build` / `test`)と diff --git a/containers/base/Dockerfile b/containers/base/Dockerfile index d242dbe..5d0b8e8 100644 --- a/containers/base/Dockerfile +++ b/containers/base/Dockerfile @@ -175,8 +175,10 @@ RUN set -eux; \ COPY --chmod=0755 tmux-first /usr/local/bin/tmux-first COPY --chmod=0755 tmux-clean /usr/local/bin/tmux-clean -RUN echo "alias tmux1='tmux-first'" >> ~/.bashrc \ - && echo "alias tmuxc='tmux-clean'" >> ~/.bashrc +# 短縮名は alias ではなく symlink で用意する。alias は bash の対話シェルにしか効かず、 +# zsh などの他シェルや非対話実行 (docker exec など) では使えないため。 +RUN sudo ln -sf tmux-first /usr/local/bin/tmux1 \ + && sudo ln -sf tmux-clean /usr/local/bin/tmuxc # entrypoint と dind COPY --chmod=755 entrypoint.sh /entrypoint.sh diff --git a/containers/base/tmux-clean b/containers/base/tmux-clean index ead403a..7e4a05d 100644 --- a/containers/base/tmux-clean +++ b/containers/base/tmux-clean @@ -129,6 +129,7 @@ has_child_process() { KILLED=0 KEPT=0 +FAILED=0 for s in $SESSIONS; do if [ "$s" = "$KEEP" ]; then echo " keep $s (keeper)" @@ -170,17 +171,25 @@ for s in $SESSIONS; do if [ "$DRY" = 1 ]; then echo " KILL $s (dry-run)" - else - # 直前にセッションが終了していた場合の競合エラーで set -e により - # 後続の削除処理が止まらないようにする。 - tmux kill-session -t "=$s" || true + KILLED=$((KILLED + 1)) + elif tmux kill-session -t "=$s" 2>/dev/null; then echo " KILL $s" + KILLED=$((KILLED + 1)) + elif ! tmux has-session -t "=$s" 2>/dev/null; then + # 直前にセッションが終了していた場合の競合。削除済みなので件数には数えない。 + echo " skip $s (既に終了していました)" + else + # 競合以外の失敗。set -e で止めずに残りを処理しつつ、利用者が検知できるよう記録する。 + echo "tmux-clean: セッションを削除できませんでした: $s" >&2 + FAILED=$((FAILED + 1)) fi - KILLED=$((KILLED + 1)) done if [ "$DRY" = 1 ]; then echo "tmux-clean: 削除対象 ${KILLED} 件 / 残す ${KEPT} 件 (dry-run のため実行していません)" else - echo "tmux-clean: 削除 ${KILLED} 件 / 残す ${KEPT} 件" + echo "tmux-clean: 削除 ${KILLED} 件 / 残す ${KEPT} 件 / 失敗 ${FAILED} 件" fi + +# 削除に失敗したセッションがあれば、呼び出し側が検知できるよう終了ステータスで知らせる。 +[ "$FAILED" = 0 ] || exit 1 diff --git a/containers/base/tmux-first b/containers/base/tmux-first index eae4981..b166767 100644 --- a/containers/base/tmux-first +++ b/containers/base/tmux-first @@ -29,7 +29,8 @@ NL=' # 最終操作からこの秒数以上経過したクライアントを「放置」とみなす。 # 負数や数値以外を許すとすべてのクライアントが idle 条件を満たしてしまい、-f なしで # 操作中の端末まで切断されるため、0 以上の整数以外は受け付けない。 -IDLE_MIN=${TMUX_FIRST_IDLE:-300} +# 既定値を使うのは未設定のときだけ。明示的な空文字は不正値として下の検証で弾く。 +IDLE_MIN=${TMUX_FIRST_IDLE-300} usage() { cat <<'USAGE' 使い方: tmux-first [-n|--dry-run] [-f|--force] [ベース名] diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index b26ca59..d78174a 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -235,14 +235,14 @@ VS Code のウィンドウが異常終了すると、VS Code サーバー側に 接続しているというだけでは居座りとは判定しません。切断対象は最終操作から `TMUX_FIRST_IDLE` 秒(既定 300)以上経過したクライアントだけで、いま使われている別ウィンドウの端末は残します(`-f` で全件切断)。 -`TMUX_FIRST_IDLE` に指定できるのは 0 以上の整数だけです。負数や数値以外を指定すると、すべてのクライアントが放置扱いになってしまうため `tmux-first` はエラー終了します。 +`TMUX_FIRST_IDLE` に指定できるのは 0 以上の整数だけです。負数・数値以外・空文字を指定すると、すべてのクライアントが放置扱いになってしまうため `tmux-first` はエラー終了します(既定値 300 が使われるのは未設定のときだけです)。 ```bash tmux-first # 現在のセッション名からベース名を推定 (adminer-2 -> adminer) tmux-first -n # 切り替えず、対象と切断予定のクライアントだけ表示する tmux-first -f # 操作中のクライアントも含めて切断する TMUX_FIRST_IDLE=60 tmux-first # 放置とみなす秒数を変える -tmux1 # tmux-first のエイリアス +tmux1 # tmux-first の短縮コマンド (/usr/local/bin の symlink) ``` 誰がいつから繋いでいるかは次で確認できます。 @@ -257,7 +257,7 @@ tmux list-clients -F '#{client_name} -> #{client_session} 最終操作 #{t:clie tmux-clean # 不要なセッションを削除 tmux-clean -n # 削除せず、削除対象と除外理由を表示する tmux-clean -f # アタッチ中・実行中のセッションも削除する -tmuxc # tmux-clean のエイリアス +tmuxc # tmux-clean の短縮コマンド (/usr/local/bin の symlink) ``` ## ソースファイル変更検出 From 29b465cf4d29d3b31226f75c4eac95480ba92f0e Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 19 Aug 2026 16:56:47 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20tmux-first=20=E3=81=AE=E5=AE=9F?= =?UTF-8?q?=E8=A1=8C=E5=85=83=E3=82=AF=E3=83=A9=E3=82=A4=E3=82=A2=E3=83=B3?= =?UTF-8?q?=E3=83=88=E8=AA=A4=E6=A4=9C=E5=87=BA=E3=81=A8=E3=80=81=E4=B8=A1?= =?UTF-8?q?=E3=82=B9=E3=82=AF=E3=83=AA=E3=83=97=E3=83=88=E3=81=AE=E5=8D=98?= =?UTF-8?q?=E8=AA=9E=E5=88=86=E5=89=B2/format=20=E7=A0=B4=E6=90=8D?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit レビュー round 4 の指摘対応。 - tmux-first: 実行元クライアントを「最終操作が SELF_FRESH(10) 秒以内」であることまで 確認できた場合のみ信頼し、確認できない場合は -f 付きでも切断せず切り替えのみ行う。 hook や send-keys 経由でキー入力を伴わずに起動された場合、従来は最終操作が最も新しい だけの無関係なクライアントを実行元とみなし、実際の利用者の端末を切断していた。 client_name と client_activity は 1 回の display-message でまとめて取得し、 switch-client にも -c で同じクライアントを渡して判定と操作の食い違いをなくす。 - tmux-first / tmux-clean: `for e in $(tmux ...)` を while read + ヒアドキュメントに 置き換え、空白による単語分割とパス名展開 (glob) を排除。 - tmux-clean: `for s in $SESSIONS` も同様に while read へ。セッション名に空白が 含まれると別名として扱われ、削除対象を取り違えていた。 - tmux-clean: `-f "#{==:#{session_name},$s}"` はセッション名に `}` や `,` が含まれると format 構文が壊れ、全セッションのアタッチ数を返して掃除が一切効かなくなるため、 `#{session_attached} #{session_name}` を awk で完全一致させる方式へ変更。 検証: sh -n / bash -n / dash -n、shellcheck (-s sh) クリーン。 実 tmux 3.7b (隔離ソケット) で、同一セッションへ 2 クライアント接続時の実行元保護、 別セッションからの ghost 切断 + 切り替え、非対話起動時の切断抑止、`we}ird` / `my proj` / `st*ar` / `com,ma` 各セッション名での keeper・アタッチ中・実行中判定を確認。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M3p9U34HsArSpxJ1E1SK94 --- CHANGELOG.md | 4 +- containers/base/tmux-clean | 54 +++++++++++++++++-------- containers/base/tmux-first | 65 ++++++++++++++++++++++++------ docs/user/environment-variables.md | 2 + 4 files changed, 96 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f41cc5..1e612ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ `-2`, `-3` ... と新しいセッションが増え続けます。`tmux-first` は放置された クライアントを切断し、一番若い番号のセッションへ現在の端末を切り替えます。切断対象は 最終操作から `TMUX_FIRST_IDLE` 秒 (未設定時のみ既定 300、0 以上の整数のみ有効) 以上経過した - クライアントだけで、使用中の端末は残します (`-f` で全件切断)。`tmux-first -n` で切り替えずに対象を確認でき、 + クライアントだけで、使用中の端末は残します (`-f` で全件切断)。実行元の端末を巻き添えに + しないよう自分自身のクライアントは必ず除外し、hook 経由など実行元を特定できない起動では + `-f` を付けていても切断せずセッションの切り替えだけを行います。`tmux-first -n` で切り替えずに対象を確認でき、 `tmux1` の短縮コマンド (symlink) も用意しています。 - **`tmux-clean` コマンドを base イメージへ追加**しました。`tmux-first` で若い番号の セッションへ戻したあと、置き去りになった `-<数字>` のセッションを削除します。 diff --git a/containers/base/tmux-clean b/containers/base/tmux-clean index 7e4a05d..cd2c81b 100644 --- a/containers/base/tmux-clean +++ b/containers/base/tmux-clean @@ -97,9 +97,15 @@ fi KEEP="" if [ -n "${TMUX:-}" ]; then CUR=$(tmux display-message -p '#S' 2>/dev/null || true) - for s in $SESSIONS; do - [ "$s" = "$CUR" ] && KEEP="$s" && break - done + # セッション名に空白や '*' が含まれても壊れないよう、for ではなく while read で回す。 + while IFS= read -r s; do + if [ -n "$s" ] && [ "$s" = "$CUR" ]; then + KEEP="$s" + break + fi + done </dev/null) + # -f "#{==:#{session_name},$s}" もセッション名に '}' や ',' が含まれると format + # 構文が壊れるため使えない。桁数の決まった #{session_attached} を先頭に置き、 + # 最初の空白より後ろ全体をセッション名として awk で完全一致させる。 + # こうすれば名前にどんな文字が入っても取り違えない。 + ATT=$(tmux list-sessions -F '#{session_attached} #{session_name}' 2>/dev/null | + awk -v s="$s" '{ + n = index($0, " ") + if (n > 0 && substr($0, n + 1) == s) { + print substr($0, 1, n - 1) + exit + } + }') BUSY="" - # pane_pid も pane_current_command も空白を含まないため ':' で連結して分解する。 - for e in $(tmux list-panes -s -t "=$s" \ - -F '#{pane_pid}:#{pane_current_command}' 2>/dev/null); do - pid=${e%%:*} - cmd=${e#*:} + # pane_pid は数字のみ、pane_current_command にも ':' は含まれないため ':' で連結する。 + # $(...) を for にそのまま渡すと空白での単語分割とパス名展開 (glob) が起きるため、 + # ヒアドキュメント + while read で 1 行ずつ読む。パイプと違いサブシェルにならないので + # ループ内で立てた BUSY がそのまま残る。 + PANES=$(tmux list-panes -s -t "=$s" \ + -F '#{pane_pid}:#{pane_current_command}' 2>/dev/null || true) + while IFS=: read -r pid cmd; do + [ -n "$pid" ] || continue if ! is_shell "$cmd"; then BUSY="$cmd" break fi - if [ -n "$pid" ] && has_child_process "$pid"; then + if has_child_process "$pid"; then BUSY="バックグラウンドジョブ (pane pid $pid)" break fi - done + done <&2 FAILED=$((FAILED + 1)) fi -done +done </dev/null || true) + # 名前と最終操作時刻は 1 回の呼び出しでまとめて取る。2 回に分けると、その間に + # 別のクライアントが操作された場合に別人の時刻を拾ってしまう。 + SELF=$(tmux display-message -p '#{client_activity}:#{client_name}' 2>/dev/null || true) + case "$SELF" in + [0-9]*:?*) + ME_ACT=${SELF%%:*} + ME=${SELF#*:} + ;; + esac + if [ -n "$ME" ] && [ "$((NOW - ME_ACT))" -le "$SELF_FRESH" ]; then + ME_VERIFIED=1 + fi fi # 対象セッションに繋がっている自分以外のクライアントのうち、最終操作から @@ -122,19 +149,20 @@ fi # クライアントを切断すると、使用中の別ウィンドウや別の利用者の端末まで # 巻き添えにしてしまうため、既定では操作中のものは残す (-f で全件切断)。 # -# tmux の中にいるのに自分のクライアントを特定できない場合は、自分自身を切断して +# tmux の中にいるのに実行元のクライアントを特定できない場合は、自分自身を切断して # しまう恐れがあるため切断対象を空にする (切り替えだけ行う)。 GHOSTS="" ALIVE="" -if [ -n "${TMUX:-}" ] && [ -z "$ME" ]; then - echo "tmux-first: 自分のクライアントを特定できないため切断は行いません" >&2 +if [ -n "${TMUX:-}" ] && [ "$ME_VERIFIED" = 0 ]; then + echo "tmux-first: 実行元のクライアントを特定できないため切断は行いません" >&2 else - NOW=$(date +%s) - # client_name も client_activity も空白を含まないため ':' で連結して分解する。 - for e in $(tmux list-clients -t "=$TARGET" \ - -F '#{client_activity}:#{client_name}' 2>/dev/null); do - act=${e%%:*} - name=${e#*:} + # client_activity は数字のみ、client_name にも ':' は含まれないため ':' で連結する。 + # $(...) を for にそのまま渡すと空白での単語分割とパス名展開 (glob) が起きるため、 + # ヒアドキュメント + while read で 1 行ずつ読む。パイプと違いサブシェルにならないので + # ループ内で組み立てた GHOSTS / ALIVE がそのまま残る。 + CLIENTS=$(tmux list-clients -t "=$TARGET" \ + -F '#{client_activity}:#{client_name}' 2>/dev/null || true) + while IFS=: read -r act name; do [ -n "$name" ] || continue [ "$name" = "${ME:-__no_client__}" ] && continue case "$act" in @@ -146,7 +174,9 @@ else else ALIVE="${ALIVE:+${ALIVE}${NL}}${name} (最終操作 ${idle} 秒前)" fi - done + done < adminer) tmux-first -n # 切り替えず、対象と切断予定のクライアントだけ表示する From f93c11add0615a87d76778cc56b71a79e063c701 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 19 Aug 2026 17:21:07 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20tmux-clean=20=E3=81=AE=E7=8A=B6?= =?UTF-8?q?=E6=85=8B=E5=8F=96=E5=BE=97=E5=A4=B1=E6=95=97=E3=82=92=20fail-c?= =?UTF-8?q?losed=20=E5=8C=96=E3=81=97=E3=80=81=E6=9C=AA=E6=A4=9C=E8=A8=BC?= =?UTF-8?q?=E3=82=AF=E3=83=A9=E3=82=A4=E3=82=A2=E3=83=B3=E3=83=88=E3=81=AE?= =?UTF-8?q?=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88=E3=82=92=E6=AD=A2=E3=82=81?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tmux-clean: list-sessions / list-panes の失敗を `|| true` で空一覧に丸めていたため、 アタッチ数も実行中コマンドも不明なまま「アイドル」と判定して既定実行で削除していた。 取得できなかった場合は削除せず keep する (-f 指定時のみ従来どおり削除)。 走査中にセッションが消えていた場合は has-session で切り分けて skip する。 - tmux-first: ME_VERIFIED=0 のとき $ME は「直近に操作された別クライアント」を指し得るのに、 切断を止めたあとで switch-client -c "$ME" によりその別クライアントを切り替えていた。 未検証時は $ME を捨て、切断だけでなく切り替えも行わず手動コマンドを案内する。 - CHANGELOG / docs/user/environment-variables.md の記述を挙動に合わせる。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M3p9U34HsArSpxJ1E1SK94 --- CHANGELOG.md | 10 ++-- containers/base/tmux-clean | 74 +++++++++++++++++++++--------- containers/base/tmux-first | 38 +++++++++------ docs/user/environment-variables.md | 4 +- 4 files changed, 84 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e612ac..b47bb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,14 +14,16 @@ 最終操作から `TMUX_FIRST_IDLE` 秒 (未設定時のみ既定 300、0 以上の整数のみ有効) 以上経過した クライアントだけで、使用中の端末は残します (`-f` で全件切断)。実行元の端末を巻き添えに しないよう自分自身のクライアントは必ず除外し、hook 経由など実行元を特定できない起動では - `-f` を付けていても切断せずセッションの切り替えだけを行います。`tmux-first -n` で切り替えずに対象を確認でき、 - `tmux1` の短縮コマンド (symlink) も用意しています。 + `-f` を付けていても切断・切り替えのどちらも行わず、手動で切り替えるコマンドだけを案内します + (実行元が特定できない状態で切り替えると、別の利用者の端末を切り替えてしまうため)。 + `tmux-first -n` で切り替えずに対象を確認でき、`tmux1` の短縮コマンド (symlink) も用意しています。 - **`tmux-clean` コマンドを base イメージへ追加**しました。`tmux-first` で若い番号の セッションへ戻したあと、置き去りになった `-<数字>` のセッションを削除します。 既定では安全側に倒し、keeper (tmux 内なら現在のセッション、外なら最小番号) と、 アタッチ中のセッション、シェル以外を実行中のセッション、`&` で起動したバックグラウンド - ジョブが残っているセッションは残します。`-n` で削除対象と除外理由を確認でき、`-f` で - すべて削除します。削除に失敗したセッションがあれば標準エラーへ出力し、終了ステータス 1 で + ジョブが残っているセッションは残します。アタッチ数や pane の状態を tmux から取得できなかった + セッションも、実行中かどうか判断できないため残します (fail-closed)。`-n` で削除対象と除外理由を + 確認でき、`-f` ですべて削除します。削除に失敗したセッションがあれば標準エラーへ出力し、終了ステータス 1 で 知らせます。`tmuxc` の短縮コマンド (symlink) も用意しています。 - **bi-tools プロジェクト用コンテナ (`containers/bi-tools`)** を追加しました。 `devbase-base` に dbt-core + dbt-bigquery(1.12 系 / Python 3.11)と Lightdash CLI diff --git a/containers/base/tmux-clean b/containers/base/tmux-clean index cd2c81b..02634fd 100644 --- a/containers/base/tmux-clean +++ b/containers/base/tmux-clean @@ -144,39 +144,71 @@ while IFS= read -r s; do continue fi + # セッションの状態 (アタッチ数 / pane) を取得できなかったときに、その「空」を + # 「誰も繋いでおらずシェルしか動いていない」と読み替えると、実際には使用中の + # セッションを既定実行で削除してしまう。取得に失敗した場合は判定材料が無いと + # みなし、削除ではなく keep に倒す (fail-closed)。 + STATE_OK=1 + # display-message の -t は target-pane を取るためセッション指定に使えない。 # -f "#{==:#{session_name},$s}" もセッション名に '}' や ',' が含まれると format # 構文が壊れるため使えない。桁数の決まった #{session_attached} を先頭に置き、 # 最初の空白より後ろ全体をセッション名として awk で完全一致させる。 # こうすれば名前にどんな文字が入っても取り違えない。 - ATT=$(tmux list-sessions -F '#{session_attached} #{session_name}' 2>/dev/null | - awk -v s="$s" '{ - n = index($0, " ") - if (n > 0 && substr($0, n + 1) == s) { - print substr($0, 1, n - 1) - exit - } - }') + ATT="" + if SESSION_LIST=$(tmux list-sessions -F '#{session_attached} #{session_name}' 2>/dev/null); then + ATT=$(printf '%s\n' "$SESSION_LIST" | + awk -v s="$s" '{ + n = index($0, " ") + if (n > 0 && substr($0, n + 1) == s) { + print substr($0, 1, n - 1) + exit + } + }') + # 一覧に載っていない = 直前に終了したか取り違えている。どちらもアタッチ数が + # 不明なので未取得として扱う。 + [ -n "$ATT" ] || STATE_OK=0 + else + STATE_OK=0 + fi + BUSY="" # pane_pid は数字のみ、pane_current_command にも ':' は含まれないため ':' で連結する。 # $(...) を for にそのまま渡すと空白での単語分割とパス名展開 (glob) が起きるため、 # ヒアドキュメント + while read で 1 行ずつ読む。パイプと違いサブシェルにならないので # ループ内で立てた BUSY がそのまま残る。 - PANES=$(tmux list-panes -s -t "=$s" \ - -F '#{pane_pid}:#{pane_current_command}' 2>/dev/null || true) - while IFS=: read -r pid cmd; do - [ -n "$pid" ] || continue - if ! is_shell "$cmd"; then - BUSY="$cmd" - break - fi - if has_child_process "$pid"; then - BUSY="バックグラウンドジョブ (pane pid $pid)" - break - fi - done </dev/null); then + while IFS=: read -r pid cmd; do + [ -n "$pid" ] || continue + if ! is_shell "$cmd"; then + BUSY="$cmd" + break + fi + if has_child_process "$pid"; then + BUSY="バックグラウンドジョブ (pane pid $pid)" + break + fi + done </dev/null; then + # 走査中にセッションが終了していた場合。削除済みなので件数には数えない。 + echo " skip $s (既に終了していました)" + continue + fi + if [ "$FORCE" = 0 ]; then + echo " keep $s (状態を取得できませんでした)" + echo "tmux-clean: セッションの状態を取得できないため削除しません: $s" >&2 + KEPT=$((KEPT + 1)) + continue + fi + fi if [ "$FORCE" = 0 ] && [ "${ATT:-0}" != 0 ]; then echo " keep $s (アタッチ中: クライアント ${ATT} 個)" diff --git a/containers/base/tmux-first b/containers/base/tmux-first index 804fdc8..c0d115b 100644 --- a/containers/base/tmux-first +++ b/containers/base/tmux-first @@ -121,7 +121,8 @@ NOW=$(date +%s) # 更新されるため、これは必ず実行元自身を指す。 # 一方 hook や send-keys 経由など、キー入力を伴わない起動では別のクライアントを # 指し得る。そこで「最終操作が SELF_FRESH 秒以内」であることまで確認できた場合だけ -# 実行元と断定し、確認できない場合は実行元自身を切断する恐れがあるため切断を行わない。 +# 実行元と断定し、確認できない場合は実行元自身や別の利用者の端末を操作する恐れが +# あるため、切断も切り替えも行わない。 # # なお pane 内で `tty` が返すのは pane の pty であって client の tty ではないため、 # `tty` と #{client_tty} の照合では実行元を特定できない (常に不一致になる)。 @@ -142,6 +143,11 @@ if [ -n "${TMUX:-}" ]; then if [ -n "$ME" ] && [ "$((NOW - ME_ACT))" -le "$SELF_FRESH" ]; then ME_VERIFIED=1 fi + # 未検証の $ME は「直近に操作されたクライアント」であって実行元とは限らず、 + # 別の利用者の端末名を保持していることがある。そのまま表示や -c の対象に使うと + # 「識別できないので切断はしない」と言いながら他人の端末を切り替えてしまうため、 + # 検証できなかった場合は実行元不明として捨てる。 + [ "$ME_VERIFIED" = 1 ] || ME="" fi # 対象セッションに繋がっている自分以外のクライアントのうち、最終操作から @@ -149,8 +155,8 @@ fi # クライアントを切断すると、使用中の別ウィンドウや別の利用者の端末まで # 巻き添えにしてしまうため、既定では操作中のものは残す (-f で全件切断)。 # -# tmux の中にいるのに実行元のクライアントを特定できない場合は、自分自身を切断して -# しまう恐れがあるため切断対象を空にする (切り替えだけ行う)。 +# tmux の中にいるのに実行元のクライアントを特定できない場合は、自分自身や別の利用者の +# 端末を操作してしまう恐れがあるため、切断も切り替えも行わない。 GHOSTS="" ALIVE="" if [ -n "${TMUX:-}" ] && [ "$ME_VERIFIED" = 0 ]; then @@ -190,7 +196,7 @@ if [ "$DRY" = 1 ]; then if [ -n "$ME" ]; then echo "self : $ME" elif [ -n "${TMUX:-}" ]; then - echo "self : (クライアント未接続)" + echo "self : (特定できません: 切断も切り替えも行いません)" else echo "self : (tmux の外)" fi @@ -214,22 +220,24 @@ if [ -n "$GHOSTS" ]; then fi if [ -n "${TMUX:-}" ]; then - # 切断対象から除外したクライアントと実際に切り替えるクライアントが食い違わないよう、 - # 特定できている場合は -c で対象を固定する (-c なしだと再度「現在のクライアント」が - # 解決され、その間に別のクライアントが操作されると別人を切り替えてしまう)。 - if [ -n "$ME" ]; then - CUR=$(tmux display-message -c "$ME" -p '#S') - else - CUR=$(tmux display-message -p '#S') + if [ -z "$ME" ]; then + # 実行元を特定できていない。-c を省いた switch-client も同じ「現在の + # クライアント」解決 (最終操作が最も新しいクライアント) を使うため、 + # 別の利用者の端末を勝手に切り替えてしまう可能性がある。疑わしいときは + # 他人の端末を触らない方針に従い、切り替えも行わず手順だけ案内する。 + echo "tmux-first: 実行元のクライアントを特定できないため切り替えも行いません" >&2 + echo "tmux-first: 手動で切り替える場合は tmux switch-client -t '=$TARGET'" >&2 + exit 0 fi + # 切断対象から除外したクライアントと実際に切り替えるクライアントが食い違わないよう、 + # -c で対象を固定する (-c なしだと再度「現在のクライアント」が解決され、その間に + # 別のクライアントが操作されると別人を切り替えてしまう)。 + CUR=$(tmux display-message -c "$ME" -p '#S') if [ "$CUR" = "$TARGET" ]; then echo "tmux-first: すでに $TARGET です" - elif [ -n "$ME" ]; then - echo "tmux-first: $TARGET へ切り替えます" - tmux switch-client -c "$ME" -t "=$TARGET" else echo "tmux-first: $TARGET へ切り替えます" - tmux switch-client -t "=$TARGET" + tmux switch-client -c "$ME" -t "=$TARGET" fi else echo "tmux-first: $TARGET にアタッチします" diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index 560631e..1b81429 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -237,7 +237,7 @@ VS Code のウィンドウが異常終了すると、VS Code サーバー側に `TMUX_FIRST_IDLE` に指定できるのは 0 以上の整数だけです。負数・数値以外・空文字を指定すると、すべてのクライアントが放置扱いになってしまうため `tmux-first` はエラー終了します(既定値 300 が使われるのは未設定のときだけです)。 -実行元の端末は必ず切断対象から除外します。ただしペイン内のシェルには「どのクライアントから起動されたか」の情報がないため、tmux が持つ「最終操作が最も新しいクライアント」を実行元とみなしています。プロンプトへ打ち込んで起動した場合はそのキー入力で最終操作が更新されるため必ず実行元自身になりますが、hook や `tmux send-keys` 経由でキー入力を伴わずに起動された場合は実行元を特定できません。特定できなかった場合は `-f` を付けていても切断を行わず、セッションの切り替えだけを行います。 +実行元の端末は必ず切断対象から除外します。ただしペイン内のシェルには「どのクライアントから起動されたか」の情報がないため、tmux が持つ「最終操作が最も新しいクライアント」を実行元とみなしています。プロンプトへ打ち込んで起動した場合はそのキー入力で最終操作が更新されるため必ず実行元自身になりますが、hook や `tmux send-keys` 経由でキー入力を伴わずに起動された場合は実行元を特定できません。特定できなかった場合は `-f` を付けていても切断を行わず、セッションの切り替えも行いません(実行元が分からないまま切り替えると、直近に操作された別の利用者の端末を切り替えてしまうためです)。このときは手動で切り替えるための `tmux switch-client` コマンドを表示します。 ```bash tmux-first # 現在のセッション名からベース名を推定 (adminer-2 -> adminer) @@ -253,7 +253,7 @@ tmux1 # tmux-first の短縮コマンド (/usr/local/bin の symlink tmux list-clients -F '#{client_name} -> #{client_session} 最終操作 #{t:client_activity}' ``` -切り替わったあと、そのセッションのプロンプトで `tmux-clean` を実行すると置き去りのセッションを削除できます。安全側の既定として、keeper(tmux 内なら現在のセッション、tmux 外なら最小番号)、アタッチ中のセッション、シェル以外を実行中のセッション、`&` で起動したバックグラウンドジョブが残っているセッションは残します。 +切り替わったあと、そのセッションのプロンプトで `tmux-clean` を実行すると置き去りのセッションを削除できます。安全側の既定として、keeper(tmux 内なら現在のセッション、tmux 外なら最小番号)、アタッチ中のセッション、シェル以外を実行中のセッション、`&` で起動したバックグラウンドジョブが残っているセッションは残します。加えて、アタッチ数や pane の一覧を tmux から取得できなかったセッションも、実行中かどうか判断できないため残します。 ```bash tmux-clean # 不要なセッションを削除