| 1 | #!/usr/bin/env python3
|
| 2 | """
|
| 3 | headless_demo.py
|
| 4 |
|
| 5 | We're using Python 3 because it supports descriptor passing.
|
| 6 |
|
| 7 | Steps:
|
| 8 |
|
| 9 | - Create socketpair() for communication between 2 processes
|
| 10 | - fork() and exec() osh --headless.
|
| 11 | - Communicate synchronously
|
| 12 | """
|
| 13 | import optparse
|
| 14 | import os
|
| 15 | import pty
|
| 16 | import socket
|
| 17 | import sys
|
| 18 |
|
| 19 | import py_fanos
|
| 20 | from py_fanos import log
|
| 21 |
|
| 22 |
|
| 23 | # EVAL x
|
| 24 | COMMANDS = [
|
| 25 | b'echo hi', # OK, and prints 'hi' to stdout file descriptor
|
| 26 | b'echo !!', # no history
|
| 27 |
|
| 28 | # Headless mode uses something like 'eval' to handle multiline commands
|
| 29 | b'echo one\necho two\necho three\n', # multiline
|
| 30 | b'echo 1;\necho 2;\necho 3\n', # with semicolons
|
| 31 |
|
| 32 | b'( \necho subshell\n)\n', # proper multiline command
|
| 33 |
|
| 34 | b'ls --color=auto', # OK, and make sure it's in color!
|
| 35 | b'read x', # OK, and x is assigned
|
| 36 | b'echo "x: $x"', # OK, we maintained state
|
| 37 | b'(', # OK, syntax error to stderr
|
| 38 | b'zzZZ', # OK, and runtime error to stderr
|
| 39 | b'declare -X', # OK, and runtime error to stderr
|
| 40 | b'echo PS1=${PS1@P}', # typical prompt command
|
| 41 | b'echo $? $PWD', # dump state. TODO: JSON?
|
| 42 |
|
| 43 | # Errors at top level
|
| 44 | b'break',
|
| 45 | b'continue',
|
| 46 |
|
| 47 | # Hm this could actually return
|
| 48 | b'return',
|
| 49 | b'exit', # This exists the process
|
| 50 | b'echo done',
|
| 51 |
|
| 52 | # What about async commands like &
|
| 53 | # I think that works the same?
|
| 54 |
|
| 55 | # Is this valid? EVAL space? I think it probably shouldn't be?
|
| 56 | b'',
|
| 57 | # What about invalid netstrings?
|
| 58 | ]
|
| 59 |
|
| 60 |
|
| 61 | def ShowDescriptorState(label):
|
| 62 | if 1:
|
| 63 | pid = os.getpid()
|
| 64 | print(label + ' (PID %d)' % pid, file=sys.stderr)
|
| 65 | os.system('ls -l /proc/%d/fd >&2' % pid)
|
| 66 |
|
| 67 |
|
| 68 | def main(argv):
|
| 69 | p = optparse.OptionParser(__doc__)
|
| 70 |
|
| 71 | # By default, the server will use the stdin/stdout/stderr of THIS CLIENT
|
| 72 | # PROCESS.
|
| 73 | p.add_option(
|
| 74 | '--stdin-file', dest='stdin_file', default='/dev/stdin',
|
| 75 | help='Where the server read stdin from')
|
| 76 | p.add_option(
|
| 77 | '--stdout-file', dest='stdout_file', default='/dev/stdout',
|
| 78 | help='Where the server should send child stdout')
|
| 79 | p.add_option(
|
| 80 | '--stderr-file', dest='stderr_file', default='/dev/stderr',
|
| 81 | help='Where the server should send child stdout')
|
| 82 | p.add_option(
|
| 83 | '--sh-binary', dest='sh_binary', default='bin/osh',
|
| 84 | help='Which shell binary to launch')
|
| 85 |
|
| 86 | # Use a terminal instead
|
| 87 | p.add_option(
|
| 88 | '--to-new-pty', dest='to_new_pty', default=False, action='store_true',
|
| 89 | help='Send the child stdout to a new PTY')
|
| 90 |
|
| 91 | opts, args = p.parse_args(argv[1:])
|
| 92 |
|
| 93 | # left: we read and write from it
|
| 94 | # right: the server we spawn reads and writes.
|
| 95 | left, right = socket.socketpair()
|
| 96 |
|
| 97 | ShowDescriptorState('parent/client BEFORE')
|
| 98 |
|
| 99 | # The server/child should inherit these descriptors
|
| 100 | os.set_inheritable(left.fileno(), True)
|
| 101 | os.set_inheritable(right.fileno(), True)
|
| 102 |
|
| 103 | child_argv = [opts.sh_binary, '--headless']
|
| 104 |
|
| 105 | ret = os.fork()
|
| 106 | if ret < 0:
|
| 107 | raise AssertionError('fork failed')
|
| 108 |
|
| 109 | elif ret == 0:
|
| 110 | left.close() # close parent end in child
|
| 111 |
|
| 112 | # osh --headless will read from stdin, and write to stdout, which are both
|
| 113 | # the RIGHT socket.
|
| 114 | os.dup2(right.fileno(), 0)
|
| 115 | os.dup2(right.fileno(), 1)
|
| 116 | right.close() # we don't need this either
|
| 117 |
|
| 118 | import time
|
| 119 | time.sleep(0.1) # prevent interleaving of parent/child state
|
| 120 | ShowDescriptorState('child/server')
|
| 121 |
|
| 122 | # never returns
|
| 123 | os.execv(child_argv[0], child_argv)
|
| 124 | else:
|
| 125 | right.close() # close child end in parent
|
| 126 |
|
| 127 | ShowDescriptorState('parent/client AFTER')
|
| 128 |
|
| 129 | master_fd, slave_fd = -1, -1
|
| 130 |
|
| 131 | if opts.to_new_pty:
|
| 132 | master_fd, slave_fd = os.openpty()
|
| 133 |
|
| 134 | stdin_fd = slave_fd
|
| 135 | stdout_fd = slave_fd
|
| 136 | stderr_fd = slave_fd
|
| 137 |
|
| 138 | log('master %d slave %d', master_fd, slave_fd)
|
| 139 | #os.close(slave_fd)
|
| 140 |
|
| 141 | else:
|
| 142 | stdin_fd = os.open(opts.stdin_file, 0)
|
| 143 | stdout_fd = os.open(opts.stdout_file, os.O_RDWR | os.O_CREAT)
|
| 144 | stderr_fd = os.open(opts.stderr_file, os.O_RDWR | os.O_CREAT)
|
| 145 |
|
| 146 | log('stdout_fd = %d', stdout_fd)
|
| 147 |
|
| 148 | # Send raw requests, for testing protocol errors
|
| 149 | if args:
|
| 150 | raw_requests = [a.encode('utf-8') for a in args]
|
| 151 |
|
| 152 | status = 0
|
| 153 | for req in raw_requests:
|
| 154 | left.send(req)
|
| 155 |
|
| 156 | try:
|
| 157 | reply = py_fanos.recv(left)
|
| 158 | except ValueError as e:
|
| 159 | log('FANOS protocol error: %s', e)
|
| 160 | break
|
| 161 |
|
| 162 | log('reply %r' % reply)
|
| 163 |
|
| 164 | if reply.startswith(b'ERROR'):
|
| 165 | status = 1
|
| 166 |
|
| 167 |
|
| 168 | if reply is None:
|
| 169 | break
|
| 170 | return status
|
| 171 |
|
| 172 | # The normal path
|
| 173 |
|
| 174 | commands = [b'GETPID']
|
| 175 | #commands = [b'EVAL echo prompt ${PS1@P}']
|
| 176 | commands.extend(b'EVAL ' + c for c in COMMANDS)
|
| 177 |
|
| 178 | for cmd in commands:
|
| 179 | py_fanos.send(left, cmd, [stdin_fd, stdout_fd, stderr_fd])
|
| 180 |
|
| 181 | try:
|
| 182 | reply = py_fanos.recv(left)
|
| 183 | except ValueError as e:
|
| 184 | log('FANOS protocol error: %s', e)
|
| 185 | break
|
| 186 |
|
| 187 | log('reply %r' % reply)
|
| 188 | if reply is None:
|
| 189 | break
|
| 190 |
|
| 191 | left.close()
|
| 192 |
|
| 193 | if master_fd != -1:
|
| 194 | # This hangs because the server still has the terminal open? Not sure
|
| 195 | # where to close it.
|
| 196 | while True:
|
| 197 | chunk = os.read(master_fd, 1024)
|
| 198 | if not chunk:
|
| 199 | break
|
| 200 | log('from pty: %r', chunk)
|
| 201 |
|
| 202 | return 0
|
| 203 |
|
| 204 |
|
| 205 | if __name__ == '__main__':
|
| 206 | try:
|
| 207 | sys.exit(main(sys.argv))
|
| 208 | except RuntimeError as e:
|
| 209 | print('FATAL: %s' % e, file=sys.stderr)
|
| 210 | sys.exit(1)
|