diff --git a/CHANGELOG.md b/CHANGELOG.md index 4144120..b47bb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ ## [Unreleased] ### Added +- **`tmux-first` コマンドを base イメージへ追加**しました。VS Code のウィンドウが + 異常終了 (クラッシュ / ホスト再起動 / 接続断) すると、VS Code サーバー側に pty が + 取り残され、tmux クライアントだけがセッションへ繋がったまま残ります。統合ターミナルの + 起動スクリプトはそのセッションを「使用中」と判定するため、`-1` に戻れず + `-2`, `-3` ... と新しいセッションが増え続けます。`tmux-first` は放置された + クライアントを切断し、一番若い番号のセッションへ現在の端末を切り替えます。切断対象は + 最終操作から `TMUX_FIRST_IDLE` 秒 (未設定時のみ既定 300、0 以上の整数のみ有効) 以上経過した + クライアントだけで、使用中の端末は残します (`-f` で全件切断)。実行元の端末を巻き添えに + しないよう自分自身のクライアントは必ず除外し、hook 経由など実行元を特定できない起動では + `-f` を付けていても切断・切り替えのどちらも行わず、手動で切り替えるコマンドだけを案内します + (実行元が特定できない状態で切り替えると、別の利用者の端末を切り替えてしまうため)。 + `tmux-first -n` で切り替えずに対象を確認でき、`tmux1` の短縮コマンド (symlink) も用意しています。 +- **`tmux-clean` コマンドを base イメージへ追加**しました。`tmux-first` で若い番号の + セッションへ戻したあと、置き去りになった `-<数字>` のセッションを削除します。 + 既定では安全側に倒し、keeper (tmux 内なら現在のセッション、外なら最小番号) と、 + アタッチ中のセッション、シェル以外を実行中のセッション、`&` で起動したバックグラウンド + ジョブが残っているセッションは残します。アタッチ数や 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 (`@lightdash/cli`) を追加し、BigQuery 上の共有 dbt プロジェクト(`dbt build` / `test`)と diff --git a/containers/base/Dockerfile b/containers/base/Dockerfile index 99b1070..5d0b8e8 100644 --- a/containers/base/Dockerfile +++ b/containers/base/Dockerfile @@ -169,6 +169,17 @@ RUN set -eux; \ mkdir -p ~/.claude; \ echo '{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"echo '\''unset AWS_REGION'\'' >> \"$CLAUDE_ENV_FILE\""}]}]}}' > ~/.claude/settings.json; +# 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 + +# 短縮名は 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 COPY --chmod=755 dind /usr/local/bin/dind diff --git a/containers/base/tmux-clean b/containers/base/tmux-clean new file mode 100644 index 0000000..02634fd --- /dev/null +++ b/containers/base/tmux-clean @@ -0,0 +1,249 @@ +#!/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 ではなく while read で回す。 + while IFS= read -r s; do + if [ -n "$s" ] && [ "$s" = "$CUR" ]; then + KEEP="$s" + break + fi + done </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 +FAILED=0 +while IFS= read -r s; do + [ -n "$s" ] || continue + if [ "$s" = "$KEEP" ]; then + echo " keep $s (keeper)" + KEPT=$((KEPT + 1)) + continue + fi + + # セッションの状態 (アタッチ数 / pane) を取得できなかったときに、その「空」を + # 「誰も繋いでおらずシェルしか動いていない」と読み替えると、実際には使用中の + # セッションを既定実行で削除してしまう。取得に失敗した場合は判定材料が無いと + # みなし、削除ではなく keep に倒す (fail-closed)。 + STATE_OK=1 + + # display-message の -t は target-pane を取るためセッション指定に使えない。 + # -f "#{==:#{session_name},$s}" もセッション名に '}' や ',' が含まれると format + # 構文が壊れるため使えない。桁数の決まった #{session_attached} を先頭に置き、 + # 最初の空白より後ろ全体をセッション名として awk で完全一致させる。 + # こうすれば名前にどんな文字が入っても取り違えない。 + 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 がそのまま残る。 + if PANES=$(tmux list-panes -s -t "=$s" \ + -F '#{pane_pid}:#{pane_current_command}' 2>/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} 個)" + 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)" + 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 +done < adminer) +# tmux-first adminer ベース名を明示 +# tmux-first -n 切り替えず、対象と切断予定のクライアントだけ表示 +# tmux-first -f 操作中のクライアントも含めて切断する +# +set -eu + +DRY=0 +FORCE=0 +BASE="" +NL=' +' +# 最終操作からこの秒数以上経過したクライアントを「放置」とみなす。 +# 負数や数値以外を許すとすべてのクライアントが idle 条件を満たしてしまい、-f なしで +# 操作中の端末まで切断されるため、0 以上の整数以外は受け付けない。 +# 既定値を使うのは未設定のときだけ。明示的な空文字は不正値として下の検証で弾く。 +IDLE_MIN=${TMUX_FIRST_IDLE-300} +usage() { + cat <<'USAGE' +使い方: tmux-first [-n|--dry-run] [-f|--force] [ベース名] + + 引数なし 現在の tmux セッション名から末尾の -数字 を除いたものをベース名にする + (tmux の外にいる場合はカレントディレクトリ名) + ベース名 明示指定 (例: tmux-first adminer) + -n 切り替えを行わず、対象セッションと切断予定のクライアントを表示する + -f 操作中のクライアントも含めて切断する + +既定では、最終操作から TMUX_FIRST_IDLE 秒 (既定 300) 以上経過したクライアントだけを +切断する。使用中の端末を巻き添えにしないための安全策。 +TMUX_FIRST_IDLE に 0 以上の整数以外 (負数・数値以外) を指定した場合はエラー終了する。 +USAGE +} + +for a in "$@"; do + case "$a" in + -n | --dry-run) DRY=1 ;; + -f | --force) FORCE=1 ;; + -h | --help) + usage + exit 0 + ;; + -*) + echo "tmux-first: 不明なオプション: $a" >&2 + usage >&2 + exit 2 + ;; + *) BASE="$a" ;; + 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 +} +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 + +NOW=$(date +%s) + +# 自分自身のクライアント (これは絶対に切断しない) +# +# pane 内のシェルには「どのクライアントから起動されたか」の情報がないため、tmux の +# 「現在のクライアント」解決 (セッションに繋がっているうち最終操作が最も新しい +# クライアント) を使う。人が端末に打ち込んで起動した場合は、そのキー入力で最終操作が +# 更新されるため、これは必ず実行元自身を指す。 +# 一方 hook や send-keys 経由など、キー入力を伴わない起動では別のクライアントを +# 指し得る。そこで「最終操作が SELF_FRESH 秒以内」であることまで確認できた場合だけ +# 実行元と断定し、確認できない場合は実行元自身や別の利用者の端末を操作する恐れが +# あるため、切断も切り替えも行わない。 +# +# なお pane 内で `tty` が返すのは pane の pty であって client の tty ではないため、 +# `tty` と #{client_tty} の照合では実行元を特定できない (常に不一致になる)。 +SELF_FRESH=10 +ME="" +ME_ACT="" +ME_VERIFIED=0 +if [ -n "${TMUX:-}" ]; then + # 名前と最終操作時刻は 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 + # 未検証の $ME は「直近に操作されたクライアント」であって実行元とは限らず、 + # 別の利用者の端末名を保持していることがある。そのまま表示や -c の対象に使うと + # 「識別できないので切断はしない」と言いながら他人の端末を切り替えてしまうため、 + # 検証できなかった場合は実行元不明として捨てる。 + [ "$ME_VERIFIED" = 1 ] || ME="" +fi + +# 対象セッションに繋がっている自分以外のクライアントのうち、最終操作から +# IDLE_MIN 秒以上経過したものだけを「居座り」とみなす。接続しているだけの +# クライアントを切断すると、使用中の別ウィンドウや別の利用者の端末まで +# 巻き添えにしてしまうため、既定では操作中のものは残す (-f で全件切断)。 +# +# tmux の中にいるのに実行元のクライアントを特定できない場合は、自分自身や別の利用者の +# 端末を操作してしまう恐れがあるため、切断も切り替えも行わない。 +GHOSTS="" +ALIVE="" +if [ -n "${TMUX:-}" ] && [ "$ME_VERIFIED" = 0 ]; then + echo "tmux-first: 実行元のクライアントを特定できないため切断は行いません" >&2 +else + # 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 + '' | *[!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 <&2 + echo "$ALIVE" | sed 's/^/ - /' >&2 +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 + if [ -n "$ALIVE" ]; then + echo "$ALIVE" | sed 's/^/keep : /' + 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 [ -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 です" + else + echo "tmux-first: $TARGET へ切り替えます" + tmux switch-client -c "$ME" -t "=$TARGET" + fi +else + echo "tmux-first: $TARGET にアタッチします" + exec tmux attach-session -t "=$TARGET" +fi diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index ca6eb76..1b81429 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -227,6 +227,41 @@ fi 反映するには `tmux kill-server` でサーバーを作り直してください(`~/.tmux.conf` はサーバー起動時にのみ読まれ、既存ペインのシェルも修正前の rc で起動しているため)。 +##### 同じプロジェクトのセッションが増え続ける場合 + +VS Code のウィンドウが異常終了すると、VS Code サーバー側に pty が取り残され、tmux クライアントだけがセッションへ繋がったまま残ります。統合ターミナルの起動スクリプトは「クライアントが繋がっているセッション = 使用中」と判定するため、`adminer-1` に戻れず `adminer-2`, `adminer-3` … と新しいセッションが増えていきます。 + +このときは dev コンテナに同梱の `tmux-first` を実行してください。放置されたクライアントを切断したうえで、一番若い番号のセッションへ現在の端末を切り替えます。 + +接続しているというだけでは居座りとは判定しません。切断対象は最終操作から `TMUX_FIRST_IDLE` 秒(既定 300)以上経過したクライアントだけで、いま使われている別ウィンドウの端末は残します(`-f` で全件切断)。 + +`TMUX_FIRST_IDLE` に指定できるのは 0 以上の整数だけです。負数・数値以外・空文字を指定すると、すべてのクライアントが放置扱いになってしまうため `tmux-first` はエラー終了します(既定値 300 が使われるのは未設定のときだけです)。 + +実行元の端末は必ず切断対象から除外します。ただしペイン内のシェルには「どのクライアントから起動されたか」の情報がないため、tmux が持つ「最終操作が最も新しいクライアント」を実行元とみなしています。プロンプトへ打ち込んで起動した場合はそのキー入力で最終操作が更新されるため必ず実行元自身になりますが、hook や `tmux send-keys` 経由でキー入力を伴わずに起動された場合は実行元を特定できません。特定できなかった場合は `-f` を付けていても切断を行わず、セッションの切り替えも行いません(実行元が分からないまま切り替えると、直近に操作された別の利用者の端末を切り替えてしまうためです)。このときは手動で切り替えるための `tmux switch-client` コマンドを表示します。 + +```bash +tmux-first # 現在のセッション名からベース名を推定 (adminer-2 -> adminer) +tmux-first -n # 切り替えず、対象と切断予定のクライアントだけ表示する +tmux-first -f # 操作中のクライアントも含めて切断する +TMUX_FIRST_IDLE=60 tmux-first # 放置とみなす秒数を変える +tmux1 # tmux-first の短縮コマンド (/usr/local/bin の symlink) +``` + +誰がいつから繋いでいるかは次で確認できます。 + +```bash +tmux list-clients -F '#{client_name} -> #{client_session} 最終操作 #{t:client_activity}' +``` + +切り替わったあと、そのセッションのプロンプトで `tmux-clean` を実行すると置き去りのセッションを削除できます。安全側の既定として、keeper(tmux 内なら現在のセッション、tmux 外なら最小番号)、アタッチ中のセッション、シェル以外を実行中のセッション、`&` で起動したバックグラウンドジョブが残っているセッションは残します。加えて、アタッチ数や pane の一覧を tmux から取得できなかったセッションも、実行中かどうか判断できないため残します。 + +```bash +tmux-clean # 不要なセッションを削除 +tmux-clean -n # 削除せず、削除対象と除外理由を表示する +tmux-clean -f # アタッチ中・実行中のセッションも削除する +tmuxc # tmux-clean の短縮コマンド (/usr/local/bin の symlink) +``` + ## ソースファイル変更検出 devbase はソースファイル(`~/.aws/config` 等)のハッシュを `.env.sources.yml` で管理しています。