Fix sidebar git branch recovery after sleep/wake (#494)

This commit is contained in:
austinpower1258 2026-02-25 15:36:51 -08:00
parent 12e9c1e317
commit 213bda5e14
4 changed files with 265 additions and 18 deletions

View file

@ -3,9 +3,9 @@
_cmux_send() {
local payload="$1"
if command -v ncat >/dev/null 2>&1; then
printf '%s\n' "$payload" | ncat -U "$CMUX_SOCKET_PATH" --send-only
printf '%s\n' "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only
elif command -v socat >/dev/null 2>&1; then
printf '%s\n' "$payload" | socat - "UNIX-CONNECT:$CMUX_SOCKET_PATH"
printf '%s\n' "$payload" | socat -T 1 - "UNIX-CONNECT:$CMUX_SOCKET_PATH"
elif command -v nc >/dev/null 2>&1; then
# Some nc builds don't support unix sockets, but keep as a last-ditch fallback.
#
@ -40,9 +40,12 @@ _CMUX_PWD_LAST_PWD="${_CMUX_PWD_LAST_PWD:-}"
_CMUX_GIT_LAST_PWD="${_CMUX_GIT_LAST_PWD:-}"
_CMUX_GIT_LAST_RUN="${_CMUX_GIT_LAST_RUN:-0}"
_CMUX_GIT_JOB_PID="${_CMUX_GIT_JOB_PID:-}"
_CMUX_GIT_JOB_STARTED_AT="${_CMUX_GIT_JOB_STARTED_AT:-0}"
_CMUX_PR_LAST_PWD="${_CMUX_PR_LAST_PWD:-}"
_CMUX_PR_LAST_RUN="${_CMUX_PR_LAST_RUN:-0}"
_CMUX_PR_JOB_PID="${_CMUX_PR_JOB_PID:-}"
_CMUX_PR_JOB_STARTED_AT="${_CMUX_PR_JOB_STARTED_AT:-0}"
_CMUX_ASYNC_JOB_TIMEOUT="${_CMUX_ASYNC_JOB_TIMEOUT:-20}"
_CMUX_PORTS_LAST_RUN="${_CMUX_PORTS_LAST_RUN:-0}"
_CMUX_TTY_NAME="${_CMUX_TTY_NAME:-}"
@ -82,6 +85,28 @@ _cmux_prompt_command() {
local now=$SECONDS
local pwd="$PWD"
# Post-wake socket writes can occasionally leave a probe process wedged.
# If one probe is stale, clear the guard so fresh async probes can resume.
if [[ -n "$_CMUX_GIT_JOB_PID" ]]; then
if ! kill -0 "$_CMUX_GIT_JOB_PID" 2>/dev/null; then
_CMUX_GIT_JOB_PID=""
_CMUX_GIT_JOB_STARTED_AT=0
elif (( _CMUX_GIT_JOB_STARTED_AT > 0 )) && (( now - _CMUX_GIT_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT )); then
_CMUX_GIT_JOB_PID=""
_CMUX_GIT_JOB_STARTED_AT=0
fi
fi
if [[ -n "$_CMUX_PR_JOB_PID" ]]; then
if ! kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
elif (( _CMUX_PR_JOB_STARTED_AT > 0 )) && (( now - _CMUX_PR_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT )); then
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
fi
fi
# Resolve TTY name once.
if [[ -z "$_CMUX_TTY_NAME" ]]; then
local t
@ -109,6 +134,7 @@ _cmux_prompt_command() {
if [[ "$pwd" != "$_CMUX_GIT_LAST_PWD" ]]; then
kill "$_CMUX_GIT_JOB_PID" >/dev/null 2>&1 || true
_CMUX_GIT_JOB_PID=""
_CMUX_GIT_JOB_STARTED_AT=0
fi
fi
@ -128,6 +154,7 @@ _cmux_prompt_command() {
fi
} >/dev/null 2>&1 &
_CMUX_GIT_JOB_PID=$!
_CMUX_GIT_JOB_STARTED_AT=$now
fi
# Pull request metadata (number/state/url):
@ -136,6 +163,7 @@ _cmux_prompt_command() {
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" ]]; then
kill "$_CMUX_PR_JOB_PID" >/dev/null 2>&1 || true
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
fi
fi
@ -169,6 +197,7 @@ _cmux_prompt_command() {
fi
} >/dev/null 2>&1 &
_CMUX_PR_JOB_PID=$!
_CMUX_PR_JOB_STARTED_AT=$now
fi
fi

View file

@ -4,9 +4,9 @@
_cmux_send() {
local payload="$1"
if command -v ncat >/dev/null 2>&1; then
print -r -- "$payload" | ncat -U "$CMUX_SOCKET_PATH" --send-only
print -r -- "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only
elif command -v socat >/dev/null 2>&1; then
print -r -- "$payload" | socat - "UNIX-CONNECT:$CMUX_SOCKET_PATH"
print -r -- "$payload" | socat -T 1 - "UNIX-CONNECT:$CMUX_SOCKET_PATH"
elif command -v nc >/dev/null 2>&1; then
# Some nc builds don't support unix sockets, but keep as a last-ditch fallback.
#
@ -41,6 +41,7 @@ typeset -g _CMUX_PWD_LAST_PWD=""
typeset -g _CMUX_GIT_LAST_PWD=""
typeset -g _CMUX_GIT_LAST_RUN=0
typeset -g _CMUX_GIT_JOB_PID=""
typeset -g _CMUX_GIT_JOB_STARTED_AT=0
typeset -g _CMUX_GIT_FORCE=0
typeset -g _CMUX_GIT_HEAD_LAST_PWD=""
typeset -g _CMUX_GIT_HEAD_PATH=""
@ -49,7 +50,9 @@ typeset -g _CMUX_HAVE_ZSTAT=0
typeset -g _CMUX_PR_LAST_PWD=""
typeset -g _CMUX_PR_LAST_RUN=0
typeset -g _CMUX_PR_JOB_PID=""
typeset -g _CMUX_PR_JOB_STARTED_AT=0
typeset -g _CMUX_PR_FORCE=0
typeset -g _CMUX_ASYNC_JOB_TIMEOUT=20
typeset -g _CMUX_PORTS_LAST_RUN=0
typeset -g _CMUX_CMD_START=0
@ -188,6 +191,30 @@ _cmux_precmd() {
local cmd_start="$_CMUX_CMD_START"
_CMUX_CMD_START=0
# Post-wake socket writes can occasionally leave a probe process wedged.
# If one probe is stale, clear the guard so fresh async probes can resume.
if [[ -n "$_CMUX_GIT_JOB_PID" ]]; then
if ! kill -0 "$_CMUX_GIT_JOB_PID" 2>/dev/null; then
_CMUX_GIT_JOB_PID=""
_CMUX_GIT_JOB_STARTED_AT=0
elif (( _CMUX_GIT_JOB_STARTED_AT > 0 )) && (( now - _CMUX_GIT_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT )); then
_CMUX_GIT_JOB_PID=""
_CMUX_GIT_JOB_STARTED_AT=0
_CMUX_GIT_FORCE=1
fi
fi
if [[ -n "$_CMUX_PR_JOB_PID" ]]; then
if ! kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
elif (( _CMUX_PR_JOB_STARTED_AT > 0 )) && (( now - _CMUX_PR_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT )); then
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
_CMUX_PR_FORCE=1
fi
fi
# CWD: keep the app in sync with the actual shell directory.
# This is also the simplest way to test sidebar directory behavior end-to-end.
if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then
@ -242,6 +269,7 @@ _cmux_precmd() {
if [[ "$pwd" != "$_CMUX_GIT_LAST_PWD" ]] || (( _CMUX_GIT_FORCE )); then
kill "$_CMUX_GIT_JOB_PID" >/dev/null 2>&1 || true
_CMUX_GIT_JOB_PID=""
_CMUX_GIT_JOB_STARTED_AT=0
else
can_launch_git=0
fi
@ -264,6 +292,7 @@ _cmux_precmd() {
fi
} >/dev/null 2>&1 &!
_CMUX_GIT_JOB_PID=$!
_CMUX_GIT_JOB_STARTED_AT=$now
fi
fi
@ -285,6 +314,7 @@ _cmux_precmd() {
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" ]] || (( _CMUX_PR_FORCE )); then
kill "$_CMUX_PR_JOB_PID" >/dev/null 2>&1 || true
_CMUX_PR_JOB_PID=""
_CMUX_PR_JOB_STARTED_AT=0
else
can_launch_pr=0
fi
@ -321,6 +351,7 @@ _cmux_precmd() {
fi
} >/dev/null 2>&1 &!
_CMUX_PR_JOB_PID=$!
_CMUX_PR_JOB_STARTED_AT=$now
fi
fi

View file

@ -1535,6 +1535,38 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
}
lifecycleSnapshotObservers.append(sessionResignObserver)
let didWakeObserver = workspaceCenter.addObserver(
forName: NSWorkspace.didWakeNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
self?.restartSocketListenerIfEnabled(source: "workspace.didWake")
}
}
lifecycleSnapshotObservers.append(didWakeObserver)
}
private func socketListenerConfigurationIfEnabled() -> (mode: SocketControlMode, path: String)? {
let raw = UserDefaults.standard.string(forKey: SocketControlSettings.appStorageKey)
?? SocketControlSettings.defaultMode.rawValue
let userMode = SocketControlSettings.migrateMode(raw)
let mode = SocketControlSettings.effectiveMode(userMode: userMode)
guard mode != .off else { return nil }
return (mode: mode, path: SocketControlSettings.socketPath())
}
private func restartSocketListenerIfEnabled(source: String) {
guard let tabManager,
let config = socketListenerConfigurationIfEnabled() else { return }
sentryBreadcrumb("socket.listener.restart", category: "socket", data: [
"mode": config.mode.rawValue,
"path": config.path,
"source": source
])
TerminalController.shared.stop()
TerminalController.shared.start(tabManager: tabManager, socketPath: config.path, accessMode: config.mode)
}
private func disableSuddenTerminationIfNeeded() {
@ -3231,28 +3263,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
@objc func restartSocketListener(_ sender: Any?) {
guard let tabManager else {
guard tabManager != nil else {
NSSound.beep()
return
}
let raw = UserDefaults.standard.string(forKey: SocketControlSettings.appStorageKey)
?? SocketControlSettings.defaultMode.rawValue
let userMode = SocketControlSettings.migrateMode(raw)
let mode = SocketControlSettings.effectiveMode(userMode: userMode)
guard mode != .off else {
guard socketListenerConfigurationIfEnabled() != nil else {
TerminalController.shared.stop()
NSSound.beep()
return
}
let socketPath = SocketControlSettings.socketPath()
sentryBreadcrumb("socket.listener.restart", category: "socket", data: [
"mode": mode.rawValue,
"path": socketPath
])
TerminalController.shared.stop()
TerminalController.shared.start(tabManager: tabManager, socketPath: socketPath, accessMode: mode)
restartSocketListenerIfEnabled(source: "menu.command")
}
private func setupMenuBarExtra() {

View file

@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Regression guard for issue #494 (post-wake sidebar git updates freezing)."""
from __future__ import annotations
import subprocess
from pathlib import Path
def get_repo_root() -> Path:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True,
text=True,
)
if result.returncode == 0:
return Path(result.stdout.strip())
return Path.cwd()
def read_text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
if needle not in content:
failures.append(message)
def main() -> int:
repo_root = get_repo_root()
zsh_path = repo_root / "Resources" / "shell-integration" / "cmux-zsh-integration.zsh"
bash_path = repo_root / "Resources" / "shell-integration" / "cmux-bash-integration.bash"
app_delegate_path = repo_root / "Sources" / "AppDelegate.swift"
required_paths = [zsh_path, bash_path, app_delegate_path]
missing_paths = [str(path) for path in required_paths if not path.exists()]
if missing_paths:
print("Missing expected files:")
for path in missing_paths:
print(f" - {path}")
return 1
zsh_content = read_text(zsh_path)
bash_content = read_text(bash_path)
app_delegate = read_text(app_delegate_path)
failures: list[str] = []
require(
zsh_content,
"_CMUX_GIT_JOB_STARTED_AT",
"zsh integration is missing git probe start tracking",
failures,
)
require(
zsh_content,
"_CMUX_PR_JOB_STARTED_AT",
"zsh integration is missing PR probe start tracking",
failures,
)
require(
zsh_content,
"_CMUX_ASYNC_JOB_TIMEOUT",
"zsh integration is missing async probe timeout guard",
failures,
)
require(
zsh_content,
"now - _CMUX_GIT_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT",
"zsh integration no longer clears stale git probe PID after timeout",
failures,
)
require(
zsh_content,
"now - _CMUX_PR_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT",
"zsh integration no longer clears stale PR probe PID after timeout",
failures,
)
require(
zsh_content,
"ncat -w 1 -U \"$CMUX_SOCKET_PATH\" --send-only",
"zsh integration missing ncat socket timeout",
failures,
)
require(
zsh_content,
"socat -T 1 - \"UNIX-CONNECT:$CMUX_SOCKET_PATH\"",
"zsh integration missing socat socket timeout",
failures,
)
require(
bash_content,
"_CMUX_GIT_JOB_STARTED_AT",
"bash integration is missing git probe start tracking",
failures,
)
require(
bash_content,
"_CMUX_PR_JOB_STARTED_AT",
"bash integration is missing PR probe start tracking",
failures,
)
require(
bash_content,
"_CMUX_ASYNC_JOB_TIMEOUT",
"bash integration is missing async probe timeout guard",
failures,
)
require(
bash_content,
"now - _CMUX_GIT_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT",
"bash integration no longer clears stale git probe PID after timeout",
failures,
)
require(
bash_content,
"now - _CMUX_PR_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT",
"bash integration no longer clears stale PR probe PID after timeout",
failures,
)
require(
bash_content,
"ncat -w 1 -U \"$CMUX_SOCKET_PATH\" --send-only",
"bash integration missing ncat socket timeout",
failures,
)
require(
bash_content,
"socat -T 1 - \"UNIX-CONNECT:$CMUX_SOCKET_PATH\"",
"bash integration missing socat socket timeout",
failures,
)
require(
app_delegate,
"NSWorkspace.didWakeNotification",
"AppDelegate is missing wake observer for socket listener recovery",
failures,
)
require(
app_delegate,
"restartSocketListenerIfEnabled(source: \"workspace.didWake\")",
"Wake observer no longer re-arms the socket listener",
failures,
)
require(
app_delegate,
"private func restartSocketListenerIfEnabled(source: String)",
"Missing shared socket-listener restart helper",
failures,
)
if failures:
print("FAIL: issue #494 regression(s) detected")
for failure in failures:
print(f"- {failure}")
return 1
print("PASS: issue #494 sleep/wake recovery guards are present")
return 0
if __name__ == "__main__":
raise SystemExit(main())