Skip to content

fix(cli): propagate exit signal callbacks#5888

Open
he-yufeng wants to merge 1 commit into
livekit:mainfrom
he-yufeng:fix/exitcli-keyboardinterrupt
Open

fix(cli): propagate exit signal callbacks#5888
he-yufeng wants to merge 1 commit into
livekit:mainfrom
he-yufeng:fix/exitcli-keyboardinterrupt

Conversation

@he-yufeng
Copy link
Copy Markdown
Contributor

Summary

Fixes #5856.

To verify

  • uv run pytest tests/test_drain_timeout.py -q
  • uv run ruff check livekit-agents\livekit\agents\cli\cli.py tests\test_drain_timeout.py
  • uv run ruff format --check livekit-agents\livekit\agents\cli\cli.py tests\test_drain_timeout.py
  • uv run python -m py_compile livekit-agents\livekit\agents\cli\cli.py tests\test_drain_timeout.py
  • git diff --check

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review



class _ExitCli(BaseException):
class _ExitCli(KeyboardInterrupt):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”΄ _ExitCli swallowed in console text mode due to except KeyboardInterrupt now catching it

Changing _ExitCli's base class from BaseException to KeyboardInterrupt causes _ExitCli to be caught by the except KeyboardInterrupt: break handler in _text_mode (cli.py:1181). Previously, _ExitCli(BaseException) would bypass that handler and propagate up to the except _ExitCli: pass in _run_console (cli.py:1563), triggering a clean shutdown. Now, _ExitCli(KeyboardInterrupt) is caught at line 1181, _text_mode returns normally, and the while True loop at cli.py:1553 simply restarts text mode. The first SIGTERM/SIGINT in console text mode is silently swallowed instead of initiating shutdown. Moreover, exit_triggered is already True after the first signal, so subsequent signals call console_worker.shutdown() without raising, leaving the main thread stuck in the prompt() β†’ readkey() blocking call.

Prompt for agents
The root cause is that _text_mode (cli.py:1168) catches KeyboardInterrupt at line 1181, and _ExitCli is now a subclass of KeyboardInterrupt, so _ExitCli raised from the signal handler gets caught there instead of propagating to the outer except _ExitCli handler in _run_console (line 1563).

To fix this, the except KeyboardInterrupt handler in _text_mode needs to distinguish between a native KeyboardInterrupt (raised by readkey() on Ctrl+C, see readchar.py:201) and _ExitCli. The simplest fix is to re-raise _ExitCli:

  In _text_mode (around line 1181), change:
    except KeyboardInterrupt:
        break
  to:
    except _ExitCli:
        raise
    except KeyboardInterrupt:
        break

This ensures _ExitCli propagates out to _run_console while still letting the native KeyboardInterrupt from readkey() break the text input loop.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@he-yufeng thanks for the pr! I think this is a real issue, maybe re-raise the _ExitCli in _text_mode first.

        try:
            text = prompt(
                Text.from_markup("  [bold]User input[/bold]: "),
                console=c.console,
                key_read_cb=_key_read,
                placeholder="Type to talk to your agent",
            )
        except _ExitCli:
            raise
        except KeyboardInterrupt:
            break

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_ExitCli still swallowed by asyncio Handle._run after PR #5519 (callback-frame race)

2 participants