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