Fix retry crash for unregistered remotes in the Sessions popup
Opening a window whose target was removed from the registry (its tab stays open by design) and failing landed in the action menu with a "重试连接" row that indexed self.entries directly and raised KeyError, killing the popup with a traceback. Only offer the retry action for registered targets, degrade a stale retry row to a message, and keep the popup alive when a handler raises anything unexpected instead of dying with a traceback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+25
-7
@@ -616,7 +616,13 @@ class SessionSelector:
|
|||||||
except curses.error:
|
except curses.error:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
outcome = self._handle_key(key, filtered)
|
try:
|
||||||
|
outcome = self._handle_key(key, filtered)
|
||||||
|
except Exception as error: # noqa: BLE001 - keep the popup alive
|
||||||
|
# A handler bug must not replace the popup with a traceback.
|
||||||
|
self._show_main()
|
||||||
|
self.message = f"内部错误:{type(error).__name__}: {error}"
|
||||||
|
continue
|
||||||
if isinstance(outcome, SelectorResult) or outcome is False:
|
if isinstance(outcome, SelectorResult) or outcome is False:
|
||||||
return outcome if isinstance(outcome, SelectorResult) else None
|
return outcome if isinstance(outcome, SelectorResult) else None
|
||||||
|
|
||||||
@@ -797,7 +803,12 @@ class SessionSelector:
|
|||||||
elif source.kind == "retry":
|
elif source.kind == "retry":
|
||||||
rows.append(_action_row(source, "retry", "action-retry", "重试连接"))
|
rows.append(_action_row(source, "retry", "action-retry", "重试连接"))
|
||||||
|
|
||||||
if source.kind != "retry" and source.error is not None and source.target is not None:
|
if (
|
||||||
|
source.kind != "retry"
|
||||||
|
and source.error is not None
|
||||||
|
and source.target is not None
|
||||||
|
and source.target in self.entries
|
||||||
|
):
|
||||||
rows.append(_action_row(source, "retry", "action-retry", "重试连接"))
|
rows.append(_action_row(source, "retry", "action-retry", "重试连接"))
|
||||||
|
|
||||||
if source.kind == "window" and source.endpoint_key is not None:
|
if source.kind == "window" and source.endpoint_key is not None:
|
||||||
@@ -1008,8 +1019,15 @@ class SessionSelector:
|
|||||||
self._show_actions(row)
|
self._show_actions(row)
|
||||||
elif row.kind in {"retry", "action-retry"}:
|
elif row.kind in {"retry", "action-retry"}:
|
||||||
assert row.target is not None
|
assert row.target is not None
|
||||||
|
entry = self.entries.get(row.target)
|
||||||
|
if entry is None:
|
||||||
|
# The window's target was removed from the registry while its
|
||||||
|
# tab stayed open; there is nothing to re-query.
|
||||||
|
self._show_main()
|
||||||
|
self.message = f"{row.target} 未登记为远端,无法重试;可通过“添加远端”重新登记"
|
||||||
|
return None
|
||||||
self.results.pop(row.target, None)
|
self.results.pop(row.target, None)
|
||||||
self.pool.enqueue(self.entries[row.target])
|
self.pool.enqueue(entry)
|
||||||
self._show_main()
|
self._show_main()
|
||||||
self.message = f"正在重试 {row.target}"
|
self.message = f"正在重试 {row.target}"
|
||||||
elif row.kind == "action-new":
|
elif row.kind == "action-new":
|
||||||
@@ -1122,7 +1140,7 @@ class SessionSelector:
|
|||||||
return result
|
return result
|
||||||
try:
|
try:
|
||||||
self.activate_handler(result)
|
self.activate_handler(result)
|
||||||
except (OSError, RuntimeError, ValueError) as error:
|
except Exception as error: # noqa: BLE001 - shown inside the popup
|
||||||
detail = str(error).strip() or "未知错误"
|
detail = str(error).strip() or "未知错误"
|
||||||
self.message = f"打开失败:{detail}"
|
self.message = f"打开失败:{detail}"
|
||||||
if row is not None:
|
if row is not None:
|
||||||
@@ -1144,7 +1162,7 @@ class SessionSelector:
|
|||||||
if self.rename_handler is not None:
|
if self.rename_handler is not None:
|
||||||
try:
|
try:
|
||||||
self.rename_handler(self.input_key, normalized)
|
self.rename_handler(self.input_key, normalized)
|
||||||
except (OSError, RuntimeError, ValueError) as error:
|
except Exception as error: # noqa: BLE001 - shown inside the popup
|
||||||
self.message = str(error)
|
self.message = str(error)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1280,7 +1298,7 @@ class SessionSelector:
|
|||||||
self.pending_action = None
|
self.pending_action = None
|
||||||
try:
|
try:
|
||||||
outcome = future.result()
|
outcome = future.result()
|
||||||
except (OSError, RuntimeError, ValueError) as error:
|
except Exception as error: # noqa: BLE001 - shown inside the popup
|
||||||
self._show_main()
|
self._show_main()
|
||||||
entry = self.entries.get(action.target)
|
entry = self.entries.get(action.target)
|
||||||
if entry is not None:
|
if entry is not None:
|
||||||
@@ -1368,7 +1386,7 @@ class SessionSelector:
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self.windows = list(self.close_window_handler(window_id))
|
self.windows = list(self.close_window_handler(window_id))
|
||||||
except (OSError, RuntimeError, ValueError) as error:
|
except Exception as error: # noqa: BLE001 - shown inside the popup
|
||||||
self.message = f"关闭 Tab 失败:{error}"
|
self.message = f"关闭 Tab 失败:{error}"
|
||||||
return
|
return
|
||||||
self._show_main()
|
self._show_main()
|
||||||
|
|||||||
@@ -1000,5 +1000,50 @@ class SelectorActionTests(unittest.TestCase):
|
|||||||
os.waitpid(pid, 0)
|
os.waitpid(pid, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectorRobustnessTests(unittest.TestCase):
|
||||||
|
def test_retry_is_not_offered_for_unregistered_remote_and_never_crashes(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
|
||||||
|
def activate(result: SelectorResult) -> None:
|
||||||
|
raise RuntimeError("ssh: connect to host orangepi port 22: No route to host")
|
||||||
|
|
||||||
|
# The registry is empty: this window's target was removed via
|
||||||
|
# "移除远端记录" while its tab stayed open.
|
||||||
|
window = CatalogWindow(
|
||||||
|
"@7",
|
||||||
|
"remote:orangepi:default",
|
||||||
|
"R orangepi/default",
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
kind="remote",
|
||||||
|
target="orangepi",
|
||||||
|
session="default",
|
||||||
|
default_label="R orangepi/default",
|
||||||
|
)
|
||||||
|
selector = SessionSelector(
|
||||||
|
[window],
|
||||||
|
RemoteRegistry(Path(directory) / "remotes.json"),
|
||||||
|
"/missing/ssh",
|
||||||
|
activate_handler=activate,
|
||||||
|
)
|
||||||
|
self.addCleanup(selector.pool.close)
|
||||||
|
row = next(
|
||||||
|
item
|
||||||
|
for item in selector._rows() # type: ignore[attr-defined]
|
||||||
|
if item.key == "window:@7"
|
||||||
|
)
|
||||||
|
self.assertIsNone(selector._activate(row)) # type: ignore[attr-defined]
|
||||||
|
self.assertEqual(selector.view, "actions")
|
||||||
|
kinds = [item.kind for item in selector._rows()] # type: ignore[attr-defined]
|
||||||
|
self.assertIn("action-window", kinds)
|
||||||
|
self.assertNotIn("action-retry", kinds)
|
||||||
|
|
||||||
|
# A stale retry row must degrade to a message instead of a KeyError.
|
||||||
|
stale = PickerRow("retry:orangepi", "retry", "! orangepi", target="orangepi")
|
||||||
|
self.assertIsNone(selector._activate(stale)) # type: ignore[attr-defined]
|
||||||
|
self.assertEqual(selector.view, "main")
|
||||||
|
self.assertIn("未登记", selector.message)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user