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)
|