OILS / core / main_loop.py View on Github | oilshell.org

414 lines, 229 significant
1"""main_loop.py.
2
3Variants:
4 main_loop.Interactive() calls ParseInteractiveLine() and ExecuteAndCatch()
5 main_loop.Batch() calls ParseLogicalLine() and ExecuteAndCatch()
6 main_loop.Headless() calls Batch() like eval and source.
7 We want 'echo 1\necho 2\n' to work, so we
8 don't bother with "the PS2 problem".
9 main_loop.ParseWholeFile() calls ParseLogicalLine(). Used by osh -n.
10"""
11from __future__ import print_function
12
13from _devbuild.gen import arg_types
14from _devbuild.gen.syntax_asdl import (command, command_t, parse_result,
15 parse_result_e)
16from core import error
17from core import process
18from display import ui
19from core import util
20from frontend import reader
21from osh import cmd_eval
22from mycpp import mylib
23from mycpp.mylib import log, print_stderr, probe, tagswitch
24
25import fanos
26import posix_ as posix
27
28from typing import cast, Any, List, TYPE_CHECKING
29if TYPE_CHECKING:
30 from core.comp_ui import _IDisplay
31 from frontend import parse_lib
32 from osh.cmd_parse import CommandParser
33 from osh.cmd_eval import CommandEvaluator
34 from osh.prompt import UserPlugin
35
36_ = log
37
38
39class ctx_Descriptors(object):
40 """Save and restore descriptor state for the headless EVAL command."""
41
42 def __init__(self, fds):
43 # type: (List[int]) -> None
44
45 self.saved0 = process.SaveFd(0)
46 self.saved1 = process.SaveFd(1)
47 self.saved2 = process.SaveFd(2)
48
49 #ShowDescriptorState('BEFORE')
50 posix.dup2(fds[0], 0)
51 posix.dup2(fds[1], 1)
52 posix.dup2(fds[2], 2)
53
54 self.fds = fds
55
56 def __enter__(self):
57 # type: () -> None
58 pass
59
60 def __exit__(self, type, value, traceback):
61 # type: (Any, Any, Any) -> None
62
63 # Restore
64 posix.dup2(self.saved0, 0)
65 posix.dup2(self.saved1, 1)
66 posix.dup2(self.saved2, 2)
67
68 # Restoration done, so close
69 posix.close(self.saved0)
70 posix.close(self.saved1)
71 posix.close(self.saved2)
72
73 # And close descriptors we were passed
74 posix.close(self.fds[0])
75 posix.close(self.fds[1])
76 posix.close(self.fds[2])
77
78
79def fanos_log(msg):
80 # type: (str) -> None
81 print_stderr('[FANOS] %s' % msg)
82
83
84def ShowDescriptorState(label):
85 # type: (str) -> None
86 if mylib.PYTHON:
87 import os # Our posix fork doesn't have os.system
88 import time
89 time.sleep(0.01) # prevent interleaving
90
91 pid = posix.getpid()
92 print_stderr(label + ' (PID %d)' % pid)
93
94 os.system('ls -l /proc/%d/fd >&2' % pid)
95
96 time.sleep(0.01) # prevent interleaving
97
98
99class Headless(object):
100 """Main loop for headless mode."""
101
102 def __init__(self, cmd_ev, parse_ctx, errfmt):
103 # type: (CommandEvaluator, parse_lib.ParseContext, ui.ErrorFormatter) -> None
104 self.cmd_ev = cmd_ev
105 self.parse_ctx = parse_ctx
106 self.errfmt = errfmt
107
108 def Loop(self):
109 # type: () -> int
110 try:
111 return self._Loop()
112 except ValueError as e:
113 fanos.send(1, 'ERROR %s' % e)
114 return 1
115
116 def EVAL(self, arg):
117 # type: (str) -> str
118
119 # This logic is similar to the 'eval' builtin in osh/builtin_meta.
120
121 # Note: we're not using the InteractiveLineReader, so there's no history
122 # expansion. It would be nice if there was a way for the client to use
123 # that.
124 line_reader = reader.StringLineReader(arg, self.parse_ctx.arena)
125 c_parser = self.parse_ctx.MakeOshParser(line_reader)
126
127 # Status is unused; $_ can be queried by the headless client
128 unused_status = Batch(self.cmd_ev, c_parser, self.errfmt, 0)
129
130 return '' # result is always 'OK ' since there was no protocol error
131
132 def _Loop(self):
133 # type: () -> int
134 fanos_log(
135 'Connect stdin and stdout to one end of socketpair() and send control messages. osh writes debug messages (like this one) to stderr.'
136 )
137
138 fd_out = [] # type: List[int]
139 while True:
140 try:
141 blob = fanos.recv(0, fd_out)
142 except ValueError as e:
143 fanos_log('protocol error: %s' % e)
144 raise # higher level handles it
145
146 if blob is None:
147 fanos_log('EOF received')
148 break
149
150 fanos_log('received blob %r' % blob)
151 if ' ' in blob:
152 bs = blob.split(' ', 1)
153 command = bs[0]
154 arg = bs[1]
155 else:
156 command = blob
157 arg = ''
158
159 if command == 'GETPID':
160 reply = str(posix.getpid())
161
162 elif command == 'EVAL':
163 #fanos_log('arg %r', arg)
164
165 if len(fd_out) != 3:
166 raise ValueError('Expected 3 file descriptors')
167
168 for fd in fd_out:
169 fanos_log('received descriptor %d' % fd)
170
171 with ctx_Descriptors(fd_out):
172 reply = self.EVAL(arg)
173
174 #ShowDescriptorState('RESTORED')
175
176 # Note: lang == 'osh' or lang == 'ysh' puts this in different modes.
177 # Do we also need 'complete --osh' and 'complete --ysh' ?
178 elif command == 'PARSE':
179 # Just parse
180 reply = 'TODO:PARSE'
181
182 else:
183 fanos_log('Invalid command %r' % command)
184 raise ValueError('Invalid command %r' % command)
185
186 fanos.send(1, b'OK %s' % reply)
187 del fd_out[:] # reset for next iteration
188
189 return 0
190
191
192def Interactive(
193 flag, # type: arg_types.main
194 cmd_ev, # type: CommandEvaluator
195 c_parser, # type: CommandParser
196 display, # type: _IDisplay
197 prompt_plugin, # type: UserPlugin
198 waiter, # type: process.Waiter
199 errfmt, # type: ui.ErrorFormatter
200):
201 # type: (...) -> int
202 status = 0
203 done = False
204 while not done:
205 mylib.MaybeCollect() # manual GC point
206
207 # - This loop has a an odd structure because we want to do cleanup
208 # after every 'break'. (The ones without 'done = True' were
209 # 'continue')
210 # - display.EraseLines() needs to be called BEFORE displaying anything, so
211 # it appears in all branches.
212
213 while True: # ONLY EXECUTES ONCE
214 quit = False
215 prompt_plugin.Run()
216 try:
217 # may raise HistoryError or ParseError
218 result = c_parser.ParseInteractiveLine()
219 UP_result = result
220 with tagswitch(result) as case:
221 if case(parse_result_e.EmptyLine):
222 display.EraseLines()
223 # POSIX shell behavior: waitpid(-1) and show job "Done"
224 # messages
225 waiter.PollNotifications()
226 quit = True
227 elif case(parse_result_e.Eof):
228 display.EraseLines()
229 done = True
230 quit = True
231 elif case(parse_result_e.Node):
232 result = cast(parse_result.Node, UP_result)
233 node = result.cmd
234 else:
235 raise AssertionError()
236
237 except util.HistoryError as e: # e.g. expansion failed
238 # Where this happens:
239 # for i in 1 2 3; do
240 # !invalid
241 # done
242 display.EraseLines()
243 print(e.UserErrorString())
244 quit = True
245 except error.Parse as e:
246 display.EraseLines()
247 errfmt.PrettyPrintError(e)
248 status = 2
249 cmd_ev.mem.SetLastStatus(status)
250 quit = True
251 except KeyboardInterrupt: # thrown by InteractiveLineReader._GetLine()
252
253 # TODO: This doesn't look right
254 # - in bin/osh - prints at beginning of line
255 # - in _bin/cxx-asan/osh without GNU readline - prints twice
256 # sometimes
257 # - with GNU readline - it is more like bin/osh
258
259 # Here we must print a newline BEFORE EraseLines()
260 print('^C')
261 if 0:
262 from core import pyos
263 pyos.FlushStdout()
264
265 display.EraseLines()
266 # http://www.tldp.org/LDP/abs/html/exitcodes.html
267 # bash gives 130, dash gives 0, zsh gives 1.
268 # Unless we SET cmd_ev.last_status, scripts see it, so don't bother now.
269 quit = True
270
271 if quit:
272 break
273
274 display.EraseLines() # Clear candidates right before executing
275
276 # to debug the slightly different interactive prasing
277 if cmd_ev.exec_opts.noexec():
278 ui.PrintAst(node, flag)
279 break
280
281 try:
282 is_return, _ = cmd_ev.ExecuteAndCatch(node, 0)
283 except KeyboardInterrupt: # issue 467, Ctrl-C during $(sleep 1)
284 is_return = False
285 display.EraseLines()
286 status = 130 # 128 + 2
287 cmd_ev.mem.SetLastStatus(status)
288 break
289
290 status = cmd_ev.LastStatus()
291
292 waiter.PollNotifications()
293
294 if is_return:
295 done = True
296 break
297
298 break # QUIT LOOP after one iteration.
299
300 # After every "logical line", no lines will be referenced by the Arena.
301 # Tokens in the LST still point to many lines, but lines with only comment
302 # or whitespace won't be reachable, so the GC will free them.
303 c_parser.arena.DiscardLines()
304
305 cmd_ev.RunPendingTraps() # Run trap handlers even if we get just ENTER
306
307 # Cleanup after every command (or failed command).
308
309 # Reset internal newline state.
310 c_parser.Reset()
311 c_parser.ResetInputObjects()
312
313 display.Reset() # clears dupes and number of lines last displayed
314
315 # TODO: Replace this with a shell hook? with 'trap', or it could be just
316 # like command_not_found. The hook can be 'echo $?' or something more
317 # complicated, i.e. with timestamps.
318 if flag.print_status:
319 print('STATUS\t%r' % status)
320
321 return status
322
323
324def Batch(cmd_ev, c_parser, errfmt, cmd_flags=0):
325 # type: (CommandEvaluator, CommandParser, ui.ErrorFormatter, int) -> int
326 """Loop for batch execution.
327
328 Returns:
329 int status, e.g. 2 on parse error
330
331 Can this be combined with interactive loop? Differences:
332
333 - Handling of parse errors.
334 - Have to detect here docs at the end?
335
336 Not a problem:
337 - Get rid of --print-status and --show-ast for now
338 - Get rid of EOF difference
339
340 TODO:
341 - Do source / eval need this?
342 - 'source' needs to parse incrementally so that aliases are respected
343 - I doubt 'eval' does! You can test it.
344 - In contrast, 'trap' should parse up front?
345 - What about $() ?
346 """
347 status = 0
348 while True:
349 probe('main_loop', 'Batch_parse_enter')
350 try:
351 node = c_parser.ParseLogicalLine() # can raise ParseError
352 if node is None: # EOF
353 c_parser.CheckForPendingHereDocs() # can raise ParseError
354 break
355 except error.Parse as e:
356 errfmt.PrettyPrintError(e)
357 status = 2
358 break
359
360 # After every "logical line", no lines will be referenced by the Arena.
361 # Tokens in the LST still point to many lines, but lines with only comment
362 # or whitespace won't be reachable, so the GC will free them.
363 c_parser.arena.DiscardLines()
364
365 # Only optimize if we're on the last line like -c "echo hi" etc.
366 if (cmd_flags & cmd_eval.IsMainProgram and
367 c_parser.line_reader.LastLineHint()):
368 cmd_flags |= cmd_eval.OptimizeSubshells
369 if not cmd_ev.exec_opts.verbose_errexit():
370 cmd_flags |= cmd_eval.MarkLastCommands
371
372 probe('main_loop', 'Batch_parse_exit')
373
374 probe('main_loop', 'Batch_execute_enter')
375 # can't optimize this because we haven't seen the end yet
376 is_return, is_fatal = cmd_ev.ExecuteAndCatch(node, cmd_flags)
377 status = cmd_ev.LastStatus()
378 # e.g. 'return' in middle of script, or divide by zero
379 if is_return or is_fatal:
380 break
381 probe('main_loop', 'Batch_execute_exit')
382
383 probe('main_loop', 'Batch_collect_enter')
384 mylib.MaybeCollect() # manual GC point
385 probe('main_loop', 'Batch_collect_exit')
386
387 return status
388
389
390def ParseWholeFile(c_parser):
391 # type: (CommandParser) -> command_t
392 """Parse an entire shell script.
393
394 This uses the same logic as Batch(). Used by:
395 - osh -n
396 - oshc translate
397 - Used by 'trap' to store code. But 'source' and 'eval' use Batch().
398
399 Note: it does NOT call DiscardLines
400 """
401 children = [] # type: List[command_t]
402 while True:
403 node = c_parser.ParseLogicalLine() # can raise ParseError
404 if node is None: # EOF
405 c_parser.CheckForPendingHereDocs() # can raise ParseError
406 break
407 children.append(node)
408
409 mylib.MaybeCollect() # manual GC point
410
411 if len(children) == 1:
412 return children[0]
413 else:
414 return command.CommandList(children)