Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,6 +69,19 @@
置き換えました。`source "$(devbase shell-rc)"` を使っているスクリプトは
`. <DEVBASE_ROOT>/bin/rc` に書き換えてください。

### Fixed
- **tmux / screen 経由のターミナルで `devbase up --open` が無言で失敗する問題を修正**
しました。VS Code はウィンドウごとに IPC ソケット (`$TMPDIR/vscode-ipc-<uuid>.sock`)
を作り直しますが、tmux サーバーはセッション作成時の環境変数を保持し続けるため、
既存セッションに再アタッチした端末では `VSCODE_IPC_HOOK_CLI` が**消えたソケットを
指したまま**になります。従来は変数の有無だけで VS Code 統合ターミナルと判定して
いたため、`code` が死んだソケットへ接続を試みて何も起きず、`_launch` が stderr を
捨てていたのでエラーも出ませんでした。ソケットの実在を確認したうえで判定し
(`_ipc_socket_alive`)、古い場合は理由を warning に出して「手元で実行するコマンドの
提示」へ degrade します。あわせて `_launch` は stderr を握り潰さないようにしました。
tmux 側の追随設定は `docs/user/environment-variables.md` の
「tmux / screen 経由で使う場合」を参照してください。

### Added
- **ワンライナー installer (`install.sh`) を新設**しました (PLAN31_1)。
`curl -fsSL https://dl.basex.jp/i | bash`
Expand Down
40 changes: 40 additions & 0 deletions docs/user/environment-variables.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -164,6 +164,7 @@ devbase はホストマシンの認証情報を自動収集し、コンテナ内
| VS Code の Remote-SSH 統合ターミナル(同一ホストの Docker) | **クライアント側(手元)の VS Code** が開く(`code` シムが委譲) |
| VS Code の Remote-SSH 統合ターミナル(**跨ホスト**: ssh 先の Docker にコンテナ) | `DEVBASE_EDITOR_SSH_HOST` 設定時にネスト URI で開く(下記「跨ホスト」参照) |
| 手元から素の SSH(VS Code 外)で接続中 | クライアントへ自動で開く公式手段が無いため、手元で実行する `code --folder-uri ...` コマンドを提示 |
| tmux / screen 経由のターミナル | `VSCODE_IPC_HOOK_CLI` が古くなっていると VS Code 統合ターミナルとみなさず、SSH 経路の「コマンド提示」へ degrade(下記「tmux / screen 経由で使う場合」参照) |
| CI / 非対話(非 TTY) / `code` 不在 | 理由を表示してスキップ(`up` 自体は成功) |

#### 跨ホスト(Windows VS Code → Remote-SSH → Mac のコンテナ)
Expand All@@ -187,6 +188,45 @@ DEVBASE_EDITOR_SSH_HOST=mac2

> 同一ホスト構成(手元 Mac/Linux で直接、または ssh 先の Docker にコンテナが無い場合)では ssh-remote ホストは付かず、従来どおりフラット URI で開きます。

#### tmux / screen 経由で使う場合

VS Code は統合ターミナルごとに `$TMPDIR/vscode-ipc-<uuid>.sock` を作り、`VSCODE_IPC_HOOK_CLI` でその場所を伝えます。`code` はこのソケット経由でクライアント側の VS Code に依頼するため、**ソケットが死んでいると `code` は何もできません**。

tmux / screen はサーバープロセスが**セッション作成時の環境変数を保持し続ける**ため、ここが噛み合いません。VS Code のウィンドウをリロードしたり開き直したりするとソケットは作り直されますが、既存の tmux セッションに再アタッチした端末は**古いパスを引き継いだまま**になります。

devbase はソケットの実在を確認してから VS Code 統合ターミナルと判定します(変数の有無だけでは判定しません)。古い場合は警告を出したうえで「手元で実行するコマンドの提示」へ degrade するので、**黙って何も起きないという状態にはなりません**。提示されたコマンドを手元で実行すれば開けます。

自動で開く状態に戻すには、tmux 側に環境変数を追随させます。`~/.tmux.conf` に以下を追記してください。

```tmux
set -ga update-environment " VSCODE_IPC_HOOK_CLI VSCODE_GIT_IPC_HANDLE VSCODE_GIT_ASKPASS_NODE VSCODE_GIT_ASKPASS_MAIN VSCODE_GIT_ASKPASS_EXTRA_ARGS VSCODE_NONCE GIT_ASKPASS BROWSER TERM_PROGRAM"
```

これで **attach のたびに**接続してきたクライアントの値でセッション環境が更新されます。ただし更新されるのはセッション環境であり、**すでに起動しているペインのシェル**には波及しません。既存ペインにも追随させたい場合は、シェルの rc(`~/.bash_profile` 等)にプロンプトフックを置きます。

```bash
if [ -n "${TMUX:-}" ]; then
_vscode_sync_env() {
# ソケットが生きている間は何もしない(サブプロセスを起動しない)
[ -n "${VSCODE_IPC_HOOK_CLI:-}" ] && [ -S "${VSCODE_IPC_HOOK_CLI}" ] && return 0
local line
while IFS= read -r line; do
case "$line" in
VSCODE_IPC_HOOK_CLI=*|VSCODE_GIT_IPC_HANDLE=*|GIT_ASKPASS=*|BROWSER=*)
export "${line%%=*}=${line#*=}" ;;
esac
done < <(tmux show-environment 2>/dev/null)
return 0
}
case ";${PROMPT_COMMAND:-};" in
*";_vscode_sync_env;"*) ;;
*) PROMPT_COMMAND="_vscode_sync_env;${PROMPT_COMMAND:-}" ;;
esac
fi
```

反映するには `tmux kill-server` でサーバーを作り直してください(`~/.tmux.conf` はサーバー起動時にのみ読まれ、既存ペインのシェルも修正前の rc で起動しているため)。

## ソースファイル変更検出

devbase はソースファイル(`~/.aws/config` 等)のハッシュを `.env.sources.yml` で管理しています。
Expand Down
69 changes: 60 additions & 9 deletions lib/devbase/editor/opener.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,10 @@
Remote-SSH 統合ターミナルでは ``code`` シムがクライアント (例: Windows) に窓を
開く。よって ``code --folder-uri <attach-uri>`` を叩くだけで実行コンテキストに
応じた正しいクライアントへ開ける。
- ただし ``VSCODE_IPC_HOOK_CLI`` は **変数が残っていても実体が消えている**ことがある
(tmux/screen のセッション再利用、VS Code ウィンドウのリロード後の古い端末など)。
存在確認まで行わないと ``code`` が死んだソケットへ繋ぎに行き無言で失敗するため、
:func:`_ipc_socket_alive` で実在を検証してから ``in_vscode`` を立てる。
- コンテナ attach URI は ``{"containerName":"/<実コンテナ名>"}`` を hex 化した
authority を持つ (:func:`build_attach_uri`)。
- **跨ホスト (手元 VS Code → Remote-SSH(host) → ssh 先の Docker 上コンテナ) では
Expand DownExpand Up@@ -67,7 +71,7 @@ class EditorContext:
"""エディタ起動先の判定に使う実行コンテキスト。"""

is_tty: bool
in_vscode: bool # VSCODE_IPC_HOOK_CLI が設定されている
in_vscode: bool # VSCODE_IPC_HOOK_CLI が *生きている* ソケットを指している
is_wsl: bool
is_ssh: bool
is_darwin: bool
Expand DownExpand Up@@ -98,21 +102,50 @@ def _detect_wsl(environ) -> bool:
return False


def _ipc_socket_alive(environ) -> bool:
"""``VSCODE_IPC_HOOK_CLI`` が **実在するソケット** を指しているか。

「変数が設定されているか」だけでは不十分。VS Code はウィンドウごとに
``$TMPDIR/vscode-ipc-<uuid>.sock`` を作り、ウィンドウを閉じる/リロードすると
削除するため、**変数だけが古いまま残る**状況が日常的に起きる:

- tmux / screen: サーバーがセッション作成時の環境変数を保持し続けるため、
同じセッションに再アタッチした端末は死んだソケットのパスを引き継ぐ
(``update-environment`` に ``VSCODE_IPC_HOOK_CLI`` を足すと緩和できる)
- VS Code ウィンドウのリロード後に残った古いシェル
- ``nohup`` / デーモン化して生き残ったプロセス

この状態を ``in_vscode=True`` と誤判定すると :func:`decide_action` が
``launch`` を選び、``code`` が死んだソケットへ接続を試みて**無言で失敗**する。
実在を確認して False に倒せば、SSH 経路なら ``print_command`` へ degrade して
ユーザが手元で実行できるコマンドを提示できる。
"""
sock = environ.get("VSCODE_IPC_HOOK_CLI")
if not sock:
return False
# os.path.exists() は OSError / ValueError を内部で捕捉して False を返すため、
# 権限エラーや不正なパスもここで「使えない」と判定される。
return os.path.exists(sock)


def detect_context(environ=None, isatty: Optional[bool] = None,
system: Optional[str] = None) -> EditorContext:
system: Optional[str] = None,
ipc_alive: Optional[bool] = None) -> EditorContext:
"""env / OS からエディタ起動先判定に必要なコンテキストを抽出する。

引数はテスト用の差し替え口。未指定なら ``os.environ`` / ``sys.stdout`` /
``platform.system()`` を用いる。
``platform.system()`` / :func:`_ipc_socket_alive` を用いる。
"""
env = os.environ if environ is None else environ
if isatty is None:
isatty = _stdout_isatty()
if system is None:
system = platform.system()
if ipc_alive is None:
ipc_alive = _ipc_socket_alive(env)
return EditorContext(
is_tty=bool(isatty),
in_vscode=bool(env.get("VSCODE_IPC_HOOK_CLI")),
in_vscode=bool(ipc_alive),
is_wsl=_detect_wsl(env),
is_ssh=any(env.get(k) for k in ("SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY")),
is_darwin=(system == "Darwin"),
Expand DownExpand Up@@ -473,28 +506,46 @@ def decide_action(ctx: EditorContext, editor_available: bool) -> OpenPlan:


def _launch(cmd: list, env: dict) -> None:
"""エディタを非ブロッキングで起動する (up プロセスを待たせない)。"""
"""エディタを非ブロッキングで起動する (up プロセスを待たせない)。

stdout は捨てるが **stderr は握り潰さない** (親へ継承する)。``code`` は IPC 接続に
失敗すると stderr にのみ理由を出すため、ここを DEVNULL にすると「何も起きないが
エラーも出ない」という最も切り分けづらい失敗になる。非ブロッキング起動なので
メッセージは up の出力に遅れて混ざり得るが、無言よりは有用。
"""
subprocess.Popen( # noqa: S603 - argv はコード生成で外部入力を渡さない
cmd, env=env,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
)


def open_editor(*, project_name: str, dev_service_name: str, workdir: str,
index: int = 1, compose_file=None,
environ=None,
isatty: Optional[bool] = None, system: Optional[str] = None,
ipc_alive: Optional[bool] = None,
launcher: Optional[Callable[[list, dict], None]] = None) -> str:
"""dev コンテナへ接続した VS Code を開く / コマンド提示 / スキップする。

戻り値は実行された action ('launch' | 'print_command' | 'skip')。例外は
握り潰して warning にし、``up`` 本体を絶対に失敗させない。``isatty`` /
``system`` は :func:`detect_context` への差し替え口 (テスト用)。``compose_file``
は実コンテナ名問い合わせ時に起動と同じ override compose を ``-f`` で渡すため。
``system`` / ``ipc_alive`` は :func:`detect_context` への差し替え口 (テスト用)。
``compose_file`` は実コンテナ名問い合わせ時に起動と同じ override compose を
``-f`` で渡すため。
"""
env = os.environ if environ is None else environ
ctx = detect_context(env, isatty=isatty, system=system)
ctx = detect_context(env, isatty=isatty, system=system, ipc_alive=ipc_alive)
# 変数だけ残って実体が消えた IPC ソケットは無言の失敗になりやすいので明示する
# (tmux セッション再利用・VS Code ウィンドウのリロード後など)。
stale_ipc = env.get("VSCODE_IPC_HOOK_CLI")
if stale_ipc and not ctx.in_vscode:
logger.warning(
"VSCODE_IPC_HOOK_CLI が指すソケットが存在しません (%s)。VS Code 統合"
"ターミナルとしては扱いません。tmux/screen のセッションを再利用している"
"場合や VS Code のウィンドウをリロードした後の古い端末で起きます。",
stale_ipc,
)
editor = resolve_editor_cmd(env) # launch 用 (which 込み・None あり得る)
display = resolve_editor_display(env) # print 用 (必ず非 None)
plan = decide_action(ctx, editor_available=bool(editor))
Expand Down
55 changes: 51 additions & 4 deletions tests/editor/test_opener.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,12 +65,33 @@ def test_detect_context_ssh(key):
assert ctx.is_ssh is True


def test_detect_context_in_vscode():
ctx = opener.detect_context(environ={"VSCODE_IPC_HOOK_CLI": "/run/x.sock"},
def test_detect_context_in_vscode(tmp_path):
# ソケットが実在するときだけ in_vscode が立つ
sock = tmp_path / "vscode-ipc-live.sock"
sock.write_text("")
ctx = opener.detect_context(environ={"VSCODE_IPC_HOOK_CLI": str(sock)},
isatty=True, system="Linux")
assert ctx.in_vscode is True


def test_detect_context_stale_ipc_socket_is_not_vscode(tmp_path):
"""変数だけ残りソケットが消えた状態 (tmux 再アタッチ等) は in_vscode=False。

ここを True に倒すと code が死んだソケットへ繋ぎに行き無言で失敗する。
"""
ctx = opener.detect_context(
environ={"VSCODE_IPC_HOOK_CLI": str(tmp_path / "gone.sock")},
isatty=True, system="Linux")
assert ctx.in_vscode is False


def test_detect_context_ipc_alive_override():
"""``ipc_alive`` 明示時は実在チェックを行わない (テスト用差し替え口)。"""
ctx = opener.detect_context(environ={"VSCODE_IPC_HOOK_CLI": "/run/x.sock"},
isatty=True, system="Linux", ipc_alive=True)
assert ctx.in_vscode is True


# ---------------------------------------------------------------------------
# is_open_enabled
# ---------------------------------------------------------------------------
Expand DownExpand Up@@ -535,7 +556,7 @@ def test_open_editor_launch_nested_uri_under_remote_ssh(monkeypatch):
environ={"VSCODE_IPC_HOOK_CLI": "/run/x.sock",
"SSH_CONNECTION": "192.168.1.16 5 192.168.1.201 22",
"DEVBASE_EDITOR_SSH_HOST": "mac2"},
isatty=True, launcher=lambda cmd, env: calls.append(cmd),
isatty=True, ipc_alive=True, launcher=lambda cmd, env: calls.append(cmd),
)
assert result == "launch"
uri = calls[0][2]
Expand All@@ -554,11 +575,37 @@ def test_open_editor_flat_uri_when_ssh_host_unset(monkeypatch):
project_name="adminer", dev_service_name="dev", workdir="/work/adminer",
environ={"VSCODE_IPC_HOOK_CLI": "/run/x.sock",
"SSH_CONNECTION": "192.168.1.16 5 192.168.1.201 22"},
isatty=True, launcher=lambda cmd, env: calls.append(cmd),
isatty=True, ipc_alive=True, launcher=lambda cmd, env: calls.append(cmd),
)
assert "@ssh-remote" not in calls[0][2]


def test_open_editor_stale_ipc_under_ssh_degrades_to_print_command(
monkeypatch, tmp_path, caplog):
"""死んだ IPC ソケット + SSH は launch せず print_command へ degrade する。

tmux セッションを再利用した端末で `devbase up --open` が「何も起きない」
という無言の失敗になっていた回帰の防止 (実在チェック導入前は launch していた)。
"""
import logging
monkeypatch.setattr(opener.shutil, "which", lambda c: "/usr/bin/code")
calls = []
with caplog.at_level(logging.INFO):
result = opener.open_editor(
project_name="adminer", dev_service_name="dev",
workdir="/work/adminer",
environ={"VSCODE_IPC_HOOK_CLI": str(tmp_path / "gone.sock"),
"SSH_CONNECTION": "192.168.1.16 5 192.168.1.201 22"},
isatty=True, launcher=lambda cmd, env: calls.append(cmd),
)
assert result == "print_command"
assert calls == []
text = "\n".join(r.getMessage() for r in caplog.records)
# 原因が分かるログが出ていること (無言の失敗にしない)
assert "VSCODE_IPC_HOOK_CLI" in text
assert "code --folder-uri" in text


def test_open_editor_skip_when_no_editor(monkeypatch):
monkeypatch.setattr(opener.shutil, "which", lambda c: None)
calls = []
Expand Down
Loading