Skip to content
Back to top
View this page
Edit this page

# Embedding Xonsh

Warning

This page is a work-in-progress stub. It currently documents only the controlling-terminal handshake for embedded use cases. A fuller guide — covering `xonsh.main.setup()`, custom `Shell` subclasses, teardown, thread safety, and integration patterns for LLM agents / TUI apps / REPLs — will appear in a later release. If you are embedding xonsh and need guidance that is not covered here, please open an issue or discussion on GitHub.

Xonsh exposes itself as a Python library in addition to the standalone `xonsh` command. Third-party projects can create a xonsh session, load xontribs and rc files, and drive the execer / shell from their own code. The public entry point for this is `xonsh.main.setup()`, documented in `xonsh.main`.

## Calling `setup()` Off the Main Thread

`xonsh.main.setup()` is safe to call from a non-main thread. Python disallows installing signal handlers from worker threads, so xonsh skips that step when it detects it is not on the main thread. Historically this path raised `ValueError: signal only works in main thread` during load (see xonsh#3689).

Consequences for embedders:

- `setup()` completes normally from any thread; the execer, session, environment and aliases work as expected.

- The `atexit()` history-flush registration still runs, so history is flushed on normal interpreter shutdown.

- Termination-signal handlers that would flush history on `SIGTERM`, `SIGHUP`, `SIGQUIT`, `SIGTSTP` etc. are not installed in this case — the host process owns signals in embedded scenarios. If you need signal-driven flushes, call `setup()` once on the main thread at startup or install your own handlers there.

## Controlling Terminal Handshake for Embedded Interactive Shells

When xonsh runs as the standalone `xonsh` command, its entry point (`xonsh.main.main()`) automatically performs a startup handshake that makes xonsh the foreground process group of its controlling terminal. This avoids a class of startup crashes and hangs in environments where the parent process does not arrange TTY ownership correctly — Flatpak / Bubblewrap sandboxes, build systems, nested containers, `systemd --user` services, some IDE terminals.

See Controlling Terminal and Foreground Process Group for the full rationale, the failure modes it fixes (`BlockingIOError` on asyncio wakeup pipe, `termios.error` EINTR, and related), the three-step signal policy, and the `XONSH_NO_FG_TAKEOVER` escape hatch.

### When the handshake runs

The handshake is invoked automatically on both of these paths:

- `xonsh.main.main()` — the default CLI entry point.

- `xonsh.main.main_xonsh()` — the inner entry point used when some code paths bypass `main`.

It is not invoked from `xonsh.main.setup()`. That means an embedded project that builds its own interactive shell by calling `setup()` (or by instantiating `Shell`, `Execer` and `XonshSession` directly) will not get the handshake for free. If your embedded xonsh runs in one of the affected environments, you need to invoke it yourself.

### How to invoke it from embedded code

The helper currently lives in `xonsh.main` as a module-private function: `_setup_controlling_terminal`. Its signature and name may change in future releases, so wrap the call in `try` / `except` — or reach out to the xonsh maintainers if you need a stable public entry point for your embedding scenario.

Call it before you start your interactive shell loop — ideally as early in your program’s startup as possible, so that any xonsh RC or xontrib code your embedder runs already has foreground ownership:

    # embedded_launcher.py
    from xonsh.main import setup

    # Acquire foreground of controlling TTY (idempotent, safe to
    # call multiple times — only the first call does real work).
    # No-op on Windows, in non-TTY contexts (pytest, piped input,
    # redirected stderr), and when XONSH_NO_FG_TAKEOVER=1 is set.
    try:
        from xonsh.main import _setup_controlling_terminal
        _setup_controlling_terminal()
    except Exception:
        pass

    # Your existing xonsh setup stays unchanged.
    setup(
        shell_type="prompt_toolkit",
        # ... your ctx, env, xontribs, aliases, etc.
    )

    # Your custom shell / REPL / agent loop starts here.

### What the helper does

The call has three possible outcomes, all handled internally:

1. Fast path — xonsh is already the foreground process group (standard case when launched from a well-behaved terminal). The function short-circuits, installs a Python no-op handler for `SIGTTIN` / `SIGTTOU` as a safety net, and returns.

2. Full handshake — xonsh transfers TTY foreground ownership to its own process group via `setpgid(0, 0)` plus `tcsetpgrp(tty, getpgrp())`. Registers an `atexit` restorer so the previous foreground group is handed back on process exit.

3. Sandbox fallback — if the handshake cannot complete (exotic sandbox, cross-session TTY, missing `CAP_SYS_ADMIN` for certain cases), the no-op handler installed in step 1 is replaced with `SIG_IGN` so that the kernel drops `SIGTTIN` / `SIGTTOU` at delivery time and they never reach Python’s asyncio wakeup pipe.

All outcomes are safe; every error path degrades cleanly. The helper never raises under normal use.

### When you should not call it

- Headless embedding. If your embedder uses `setup(shell_type="none")` (the default) and never starts an interactive prompt, you do not need the handshake. There is no TTY to acquire. Calling the helper in this case is harmless (it will fall into the non-TTY gate and do nothing), but it is also pointless.

- You manage TTY / job control yourself. If your embedder deliberately keeps xonsh in the background of its controlling TTY — for example, you run xonsh under a PTY proxy that multiplexes multiple children — taking foreground ownership will break your design. Set `XONSH_NO_FG_TAKEOVER=1` in the environment before starting xonsh, or simply skip the call.

- You are running tests. Most test runners capture stderr, in which case the helper’s internal `isatty` gate makes the call a no-op. If you use `pytest -s` and really want the handshake disabled for tests, export `XONSH_NO_FG_TAKEOVER=1` for the test session.