Fix sidebar git branch recovery after sleep/wake (#494)
This commit is contained in:
parent
12e9c1e317
commit
213bda5e14
4 changed files with 265 additions and 18 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
166
tests/test_issue_494_sleep_wake_git_branch_recovery.py
Normal file
166
tests/test_issue_494_sleep_wake_git_branch_recovery.py
Normal 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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue