aboutsummaryrefslogtreecommitdiff
path: root/misc/runinpty.py
blob: a7649b735f78262de3f3271151246c3ade230c57 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights Reserved
# SPDX-FileCopyrightText: 2024 Jade Lovelace
# SPDX-License-Identifier: LGPL-2.1-or-later
"""
This script exists to lose Lix a dependency on expect(1) for the ability to run
something in a pty.

Yes, it could be replaced by script(1) but macOS and Linux script(1) have
diverged sufficiently badly that even specifying a subcommand to run is not the
same.
"""
import pty
import sys
import os
from termios import ONLCR, ONLRET, ONOCR, OPOST, TCSAFLUSH, tcgetattr, tcsetattr
from tty import setraw
import termios

def setup_terminal():
    # does not matter which fd we use because we are in a fresh pty
    modi = tcgetattr(pty.STDOUT_FILENO)
    [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] = modi

    # Turning \n into \r\n is not cool, Linux!
    oflag &= ~ONLCR
    # I don't know what "implementation dependent postprocessing means" but it
    # sounds bad
    oflag &= ~OPOST
    # Assume that NL performs the role of CR; do not insert CRs at column 0
    oflag |= ONLRET | ONOCR

    modi = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]

    tcsetattr(pty.STDOUT_FILENO, TCSAFLUSH, modi)


def spawn(argv: list[str]):
    """
    As opposed to pty.spawn, this one more seriously controls the pty settings.
    Necessary to turn off such fun functionality as onlcr (LF to CRLF).

    This is essentially copy pasted from pty.spawn, since there is no way to
    hook the child pre-execve
    """
    pid, master_fd = pty.fork()
    if pid == pty.CHILD:
        setup_terminal()
        os.execlp(argv[0], *argv)

    try:
        mode = tcgetattr(pty.STDIN_FILENO)
        setraw(pty.STDIN_FILENO)
        restore = True
    except termios.error:
        restore = False

    try:
        pty._copy(master_fd, pty._read, pty._read) # type: ignore
    finally:
        if restore:
            tcsetattr(pty.STDIN_FILENO, TCSAFLUSH, mode) # type: ignore

    os.close(master_fd)
    return os.waitpid(pid, 0)[1]


def main():
    if len(sys.argv) == 1:
        print(f'Usage: {sys.argv[0]} [command args]', file=sys.stderr)
        sys.exit(1)

    sys.exit(os.waitstatus_to_exitcode(spawn(sys.argv[1:])))


if __name__ == '__main__':
    main()