1 | """
|
2 | dev.py - Devtools / introspection.
|
3 | """
|
4 | from __future__ import print_function
|
5 |
|
6 | from _devbuild.gen.option_asdl import option_i, builtin_i, builtin_t
|
7 | from _devbuild.gen.runtime_asdl import (cmd_value, scope_e, trace, trace_e,
|
8 | trace_t)
|
9 | from _devbuild.gen.syntax_asdl import assign_op_e, Token
|
10 | from _devbuild.gen.value_asdl import (value, value_e, value_t, sh_lvalue,
|
11 | sh_lvalue_e, LeftName)
|
12 |
|
13 | from core import bash_impl
|
14 | from core import error
|
15 | from core import bash_impl
|
16 | from core import optview
|
17 | from core import num
|
18 | from core import state
|
19 | from display import ui
|
20 | from data_lang import j8
|
21 | from frontend import location
|
22 | from osh import word_
|
23 | from data_lang import j8_lite
|
24 | from pylib import os_path
|
25 | from mycpp import mops
|
26 | from mycpp import mylib
|
27 | from mycpp.mylib import tagswitch, iteritems, print_stderr, log
|
28 |
|
29 | import posix_ as posix
|
30 |
|
31 | from typing import List, Dict, Optional, Any, cast, TYPE_CHECKING
|
32 | if TYPE_CHECKING:
|
33 | from _devbuild.gen.syntax_asdl import assign_op_t, CompoundWord
|
34 | from _devbuild.gen.runtime_asdl import scope_t
|
35 | from _devbuild.gen.value_asdl import sh_lvalue_t
|
36 | from core import alloc
|
37 | from core.error import _ErrorWithLocation
|
38 | from core import process
|
39 | from core import util
|
40 | from frontend.parse_lib import ParseContext
|
41 | from osh.word_eval import NormalWordEvaluator
|
42 | from osh.cmd_eval import CommandEvaluator
|
43 |
|
44 | _ = log
|
45 |
|
46 |
|
47 | class CrashDumper(object):
|
48 | """Controls if we collect a crash dump, and where we write it to.
|
49 |
|
50 | An object that can be serialized to JSON.
|
51 |
|
52 | trap CRASHDUMP upload-to-server
|
53 |
|
54 | # it gets written to a file first
|
55 | upload-to-server() {
|
56 | local path=$1
|
57 | curl -X POST https://osh-trace.oilshell.org < $path
|
58 | }
|
59 |
|
60 | Things to dump:
|
61 | CommandEvaluator
|
62 | functions, aliases, traps, completion hooks, fd_state, dir_stack
|
63 |
|
64 | debug info for the source? Or does that come elsewhere?
|
65 |
|
66 | Yeah I think you should have two separate files.
|
67 | - debug info for a given piece of code (needs hash)
|
68 | - this could just be the raw source files? Does it need anything else?
|
69 | - I think it needs a hash so the VM dump can refer to it.
|
70 | - vm dump.
|
71 | - Combine those and you get a UI.
|
72 |
|
73 | One is constant at build time; the other is constant at runtime.
|
74 | """
|
75 |
|
76 | def __init__(self, crash_dump_dir, fd_state):
|
77 | # type: (str, process.FdState) -> None
|
78 | self.crash_dump_dir = crash_dump_dir
|
79 | self.fd_state = fd_state
|
80 |
|
81 | # whether we should collect a dump, at the highest level of the stack
|
82 | self.do_collect = bool(crash_dump_dir)
|
83 | self.collected = False # whether we have anything to dump
|
84 |
|
85 | self.var_stack = None # type: List[value_t]
|
86 | self.argv_stack = None # type: List[value_t]
|
87 | self.debug_stack = None # type: List[value_t]
|
88 | self.error = None # type: Dict[str, value_t]
|
89 |
|
90 | def MaybeRecord(self, cmd_ev, err):
|
91 | # type: (CommandEvaluator, _ErrorWithLocation) -> None
|
92 | """Collect data for a crash dump.
|
93 |
|
94 | Args:
|
95 | cmd_ev: CommandEvaluator instance
|
96 | error: _ErrorWithLocation (ParseError or error.FatalRuntime)
|
97 | """
|
98 | if not self.do_collect: # Either we already did it, or there is no file
|
99 | return
|
100 |
|
101 | self.var_stack, self.argv_stack, self.debug_stack = cmd_ev.mem.Dump()
|
102 | blame_tok = location.TokenFor(err.location)
|
103 |
|
104 | self.error = {
|
105 | 'msg': value.Str(err.UserErrorString()),
|
106 | }
|
107 |
|
108 | if blame_tok:
|
109 | # Could also do msg % args separately, but JavaScript won't be able to
|
110 | # render that.
|
111 | self.error['source'] = value.Str(
|
112 | ui.GetLineSourceString(blame_tok.line))
|
113 | self.error['line_num'] = num.ToBig(blame_tok.line.line_num)
|
114 | self.error['line'] = value.Str(blame_tok.line.content)
|
115 |
|
116 | # TODO: Collect functions, aliases, etc.
|
117 | self.do_collect = False
|
118 | self.collected = True
|
119 |
|
120 | def MaybeDump(self, status):
|
121 | # type: (int) -> None
|
122 | """Write the dump as JSON.
|
123 |
|
124 | User can configure it two ways:
|
125 | - dump unconditionally -- a daily cron job. This would be fine.
|
126 | - dump on non-zero exit code
|
127 |
|
128 | OILS_FAIL
|
129 | Maybe counters are different than failure
|
130 |
|
131 | OILS_CRASH_DUMP='function alias trap completion stack' ?
|
132 | OILS_COUNTER_DUMP='function alias trap completion'
|
133 | and then
|
134 | I think both of these should dump the (path, mtime, checksum) of the source
|
135 | they ran? And then you can match those up with source control or whatever?
|
136 | """
|
137 | if not self.collected:
|
138 | return
|
139 |
|
140 | my_pid = posix.getpid() # Get fresh PID here
|
141 |
|
142 | # Other things we need: the reason for the crash! _ErrorWithLocation is
|
143 | # required I think.
|
144 | d = {
|
145 | 'var_stack': value.List(self.var_stack),
|
146 | 'argv_stack': value.List(self.argv_stack),
|
147 | 'debug_stack': value.List(self.debug_stack),
|
148 | 'error': value.Dict(self.error),
|
149 | 'status': num.ToBig(status),
|
150 | 'pid': num.ToBig(my_pid),
|
151 | } # type: Dict[str, value_t]
|
152 |
|
153 | path = os_path.join(self.crash_dump_dir,
|
154 | '%d-osh-crash-dump.json' % my_pid)
|
155 |
|
156 | # TODO: This should be JSON with unicode replacement char?
|
157 | buf = mylib.BufWriter()
|
158 | j8.PrintMessage(value.Dict(d), buf, 2)
|
159 | json_str = buf.getvalue()
|
160 |
|
161 | try:
|
162 | f = self.fd_state.OpenForWrite(path)
|
163 | except (IOError, OSError) as e:
|
164 | # Ignore error
|
165 | return
|
166 |
|
167 | f.write(json_str)
|
168 |
|
169 | # TODO: mylib.Writer() needs close()? Also for DebugFile()
|
170 | #f.close()
|
171 |
|
172 | print_stderr('[%d] Wrote crash dump to %s' % (my_pid, path))
|
173 |
|
174 |
|
175 | class ctx_Tracer(object):
|
176 | """A stack for tracing synchronous constructs."""
|
177 |
|
178 | def __init__(self, tracer, label, argv):
|
179 | # type: (Tracer, str, Optional[List[str]]) -> None
|
180 | self.arg = None # type: Optional[str]
|
181 | if label in ('proc', 'module-invoke'):
|
182 | self.arg = argv[0]
|
183 | elif label in ('source', 'use'):
|
184 | self.arg = argv[1]
|
185 |
|
186 | tracer.PushMessage(label, argv)
|
187 | self.label = label
|
188 | self.tracer = tracer
|
189 |
|
190 | def __enter__(self):
|
191 | # type: () -> None
|
192 | pass
|
193 |
|
194 | def __exit__(self, type, value, traceback):
|
195 | # type: (Any, Any, Any) -> None
|
196 | self.tracer.PopMessage(self.label, self.arg)
|
197 |
|
198 |
|
199 | def _PrintShValue(val, buf):
|
200 | # type: (value_t, mylib.BufWriter) -> None
|
201 | """Print ShAssignment values.
|
202 |
|
203 | NOTE: This is a bit like _PrintVariables for declare -p
|
204 | """
|
205 | # I think this should never happen because it's for ShAssignment
|
206 | result = '?'
|
207 |
|
208 | # Using maybe_shell_encode() because it's shell
|
209 | UP_val = val
|
210 | with tagswitch(val) as case:
|
211 | if case(value_e.Str):
|
212 | val = cast(value.Str, UP_val)
|
213 | result = j8_lite.MaybeShellEncode(val.s)
|
214 |
|
215 | elif case(value_e.BashArray):
|
216 | val = cast(value.BashArray, UP_val)
|
217 | result = bash_impl.BashArray_ToStrForShellPrint(val, None)
|
218 |
|
219 | elif case(value_e.BashAssoc):
|
220 | val = cast(value.BashAssoc, UP_val)
|
221 | result = bash_impl.BashAssoc_ToStrForShellPrint(val)
|
222 |
|
223 | elif case(value_e.SparseArray):
|
224 | val = cast(value.SparseArray, UP_val)
|
225 | result = bash_impl.SparseArray_ToStrForShellPrint(val)
|
226 |
|
227 | buf.write(result)
|
228 |
|
229 |
|
230 | def PrintShellArgv(argv, buf):
|
231 | # type: (List[str], mylib.BufWriter) -> None
|
232 | for i, arg in enumerate(argv):
|
233 | if i != 0:
|
234 | buf.write(' ')
|
235 | buf.write(j8_lite.MaybeShellEncode(arg))
|
236 |
|
237 |
|
238 | def _PrintYshArgv(argv, buf):
|
239 | # type: (List[str], mylib.BufWriter) -> None
|
240 |
|
241 | # We're printing $'hi\n' for OSH, but we might want to print u'hi\n' or
|
242 | # b'\n' for YSH. We could have a shopt --set xtrace_j8 or something.
|
243 | #
|
244 | # This used to be xtrace_rich, but I think that was too subtle.
|
245 |
|
246 | for arg in argv:
|
247 | buf.write(' ')
|
248 | # TODO: use unquoted -> POSIX '' -> b''
|
249 | # This would use JSON "", which CONFLICTS with shell. So we need
|
250 | # another function.
|
251 | #j8.EncodeString(arg, buf, unquoted_ok=True)
|
252 |
|
253 | buf.write(j8_lite.MaybeShellEncode(arg))
|
254 | buf.write('\n')
|
255 |
|
256 |
|
257 | class MultiTracer(object):
|
258 | """ Manages multi-process tracing and dumping.
|
259 |
|
260 | Use case:
|
261 |
|
262 | TODO: write a shim for everything that autoconf starts out with
|
263 |
|
264 | (1) How do you discover what is shelled out to?
|
265 | - you need a MULTIPROCESS tracing and MULTIPROCESS errors
|
266 |
|
267 | OILS_TRACE_DIR=_tmp/foo OILS_TRACE_STREAMS=xtrace:completion:gc \
|
268 | OILS_TRACE_DUMPS=crash:argv0 \
|
269 | osh ./configure
|
270 |
|
271 | - Streams are written continuously, they are O(n)
|
272 | - Dumps are written once per shell process, they are O(1). This includes metrics.
|
273 |
|
274 | (2) Use that dump to generate stubs in _tmp/stubs
|
275 | They will invoke benchmarks/time-helper, so we get timing and memory use
|
276 | for each program.
|
277 |
|
278 | (3) ORIG_PATH=$PATH PATH=_tmp/stubs:$PATH osh ./configure
|
279 |
|
280 | THen the stub looks like this?
|
281 |
|
282 | #!/bin/sh
|
283 | # _tmp/stubs/cc1
|
284 |
|
285 | PATH=$ORIG_PATH time-helper -x -e -- cc1 "$@"
|
286 | """
|
287 |
|
288 | def __init__(self, shell_pid, out_dir, dumps, streams, fd_state):
|
289 | # type: (int, str, str, str, process.FdState) -> None
|
290 | """
|
291 | out_dir could be auto-generated from root PID?
|
292 | """
|
293 | # All of these may be empty string
|
294 | self.out_dir = out_dir
|
295 | self.dumps = dumps
|
296 | self.streams = streams
|
297 | self.fd_state = fd_state
|
298 |
|
299 | self.this_pid = shell_pid
|
300 |
|
301 | # This is what we consider an O(1) metric. Technically a shell program
|
302 | # could run forever and keep invoking different binaries, but that is
|
303 | # unlikely. I guess we could limit it to 1,000 or 10,000 artifically
|
304 | # or something.
|
305 | self.hist_argv0 = {} # type: Dict[str, int]
|
306 |
|
307 | def OnNewProcess(self, child_pid):
|
308 | # type: (int) -> None
|
309 | """
|
310 | Right now we call this from
|
311 | Process::StartProcess -> tracer.SetChildPid()
|
312 | It would be more accurate to call it from SubProgramThunk.
|
313 |
|
314 | TODO: do we need a compound PID?
|
315 | """
|
316 | self.this_pid = child_pid
|
317 | # each process keep track of direct children
|
318 | self.hist_argv0.clear()
|
319 |
|
320 | def EmitArgv0(self, argv0):
|
321 | # type: (str) -> None
|
322 |
|
323 | # TODO: Should we have word 0 in the source, and the FILE the $PATH
|
324 | # lookup resolved to?
|
325 |
|
326 | if argv0 not in self.hist_argv0:
|
327 | self.hist_argv0[argv0] = 1
|
328 | else:
|
329 | # TODO: mycpp doesn't allow +=
|
330 | self.hist_argv0[argv0] = self.hist_argv0[argv0] + 1
|
331 |
|
332 | def WriteDumps(self):
|
333 | # type: () -> None
|
334 | if len(self.out_dir) == 0:
|
335 | return
|
336 |
|
337 | # TSV8 table might be nicer for this
|
338 |
|
339 | metric_argv0 = [] # type: List[value_t]
|
340 | for argv0, count in iteritems(self.hist_argv0):
|
341 | a = value.Str(argv0)
|
342 | c = value.Int(mops.IntWiden(count))
|
343 | d = {'argv0': a, 'count': c}
|
344 | metric_argv0.append(value.Dict(d))
|
345 |
|
346 | # Other things we need: the reason for the crash! _ErrorWithLocation is
|
347 | # required I think.
|
348 | j = {
|
349 | 'pid': value.Int(mops.IntWiden(self.this_pid)),
|
350 | 'metric_argv0': value.List(metric_argv0),
|
351 | } # type: Dict[str, value_t]
|
352 |
|
353 | # dumps are named $PID.$channel.json
|
354 | path = os_path.join(self.out_dir, '%d.argv0.json' % self.this_pid)
|
355 |
|
356 | buf = mylib.BufWriter()
|
357 | j8.PrintMessage(value.Dict(j), buf, 2)
|
358 | json8_str = buf.getvalue()
|
359 |
|
360 | try:
|
361 | f = self.fd_state.OpenForWrite(path)
|
362 | except (IOError, OSError) as e:
|
363 | # Ignore error
|
364 | return
|
365 |
|
366 | f.write(json8_str)
|
367 | f.close()
|
368 |
|
369 | print_stderr('[%d] Wrote metrics dump to %s' % (self.this_pid, path))
|
370 |
|
371 |
|
372 | class Tracer(object):
|
373 | """For OSH set -x, and YSH hierarchical, parsable tracing.
|
374 |
|
375 | See doc/xtrace.md for details.
|
376 |
|
377 | - TODO: Connect it somehow to tracers for other processes. So you can make
|
378 | an HTML report offline.
|
379 | - Could inherit SHX_*
|
380 |
|
381 | https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html#Bash-Variables
|
382 |
|
383 | Other hooks:
|
384 |
|
385 | - Command completion starts other processes
|
386 | - YSH command constructs: BareDecl, VarDecl, Mutation, Expr
|
387 | """
|
388 |
|
389 | def __init__(
|
390 | self,
|
391 | parse_ctx, # type: ParseContext
|
392 | exec_opts, # type: optview.Exec
|
393 | mutable_opts, # type: state.MutableOpts
|
394 | mem, # type: state.Mem
|
395 | f, # type: util._DebugFile
|
396 | multi_trace, # type: MultiTracer
|
397 | ):
|
398 | # type: (...) -> None
|
399 | """
|
400 | trace_dir comes from OILS_TRACE_DIR
|
401 | """
|
402 | self.parse_ctx = parse_ctx
|
403 | self.exec_opts = exec_opts
|
404 | self.mutable_opts = mutable_opts
|
405 | self.mem = mem
|
406 | self.f = f # can be stderr, the --debug-file, etc.
|
407 | self.multi_trace = multi_trace
|
408 |
|
409 | self.word_ev = None # type: NormalWordEvaluator
|
410 |
|
411 | self.ind = 0 # changed by process, proc, source, eval
|
412 | self.indents = [''] # "pooled" to avoid allocations
|
413 |
|
414 | # PS4 value -> CompoundWord. PS4 is scoped.
|
415 | self.parse_cache = {} # type: Dict[str, CompoundWord]
|
416 |
|
417 | # Mutate objects to save allocations
|
418 | self.val_indent = value.Str('')
|
419 | self.val_punct = value.Str('')
|
420 | # TODO: show something for root process by default? INTERLEAVED output
|
421 | # can be confusing, e.g. debugging traps in forkred subinterpreter
|
422 | # created by a pipeline.
|
423 | self.val_pid_str = value.Str('') # mutated by SetProcess
|
424 |
|
425 | # Can these be global constants? I don't think we have that in ASDL yet.
|
426 | self.lval_indent = location.LName('SHX_indent')
|
427 | self.lval_punct = location.LName('SHX_punct')
|
428 | self.lval_pid_str = location.LName('SHX_pid_str')
|
429 |
|
430 | def CheckCircularDeps(self):
|
431 | # type: () -> None
|
432 | assert self.word_ev is not None
|
433 |
|
434 | def _EvalPS4(self, punct):
|
435 | # type: (str) -> str
|
436 | """The prefix of each line."""
|
437 | val = self.mem.GetValue('PS4')
|
438 | if val.tag() == value_e.Str:
|
439 | ps4 = cast(value.Str, val).s
|
440 | else:
|
441 | ps4 = ''
|
442 |
|
443 | # NOTE: This cache is slightly broken because aliases are mutable! I think
|
444 | # that is more or less harmless though.
|
445 | ps4_word = self.parse_cache.get(ps4)
|
446 | if ps4_word is None:
|
447 | # We have to parse this at runtime. PS4 should usually remain constant.
|
448 | w_parser = self.parse_ctx.MakeWordParserForPlugin(ps4)
|
449 |
|
450 | # NOTE: could use source.Variable, like $PS1 prompt does
|
451 | try:
|
452 | ps4_word = w_parser.ReadForPlugin()
|
453 | except error.Parse as e:
|
454 | ps4_word = word_.ErrorWord("<ERROR: Can't parse PS4: %s>" %
|
455 | e.UserErrorString())
|
456 | self.parse_cache[ps4] = ps4_word
|
457 |
|
458 | # Mutate objects to save allocations
|
459 | if self.exec_opts.xtrace_rich():
|
460 | self.val_indent.s = self.indents[self.ind]
|
461 | else:
|
462 | self.val_indent.s = ''
|
463 | self.val_punct.s = punct
|
464 |
|
465 | # Prevent infinite loop when PS4 has command sub!
|
466 | assert self.exec_opts.xtrace() # We shouldn't call this unless it's on
|
467 |
|
468 | # TODO: Remove allocation for [] ?
|
469 | with state.ctx_Option(self.mutable_opts, [option_i.xtrace], False):
|
470 | with state.ctx_Temp(self.mem):
|
471 | self.mem.SetNamed(self.lval_indent, self.val_indent,
|
472 | scope_e.LocalOnly)
|
473 | self.mem.SetNamed(self.lval_punct, self.val_punct,
|
474 | scope_e.LocalOnly)
|
475 | self.mem.SetNamed(self.lval_pid_str, self.val_pid_str,
|
476 | scope_e.LocalOnly)
|
477 | prefix = self.word_ev.EvalForPlugin(ps4_word)
|
478 | return prefix.s
|
479 |
|
480 | def _Inc(self):
|
481 | # type: () -> None
|
482 | self.ind += 1
|
483 | if self.ind >= len(self.indents): # make sure there are enough
|
484 | self.indents.append(' ' * self.ind)
|
485 |
|
486 | def _Dec(self):
|
487 | # type: () -> None
|
488 | self.ind -= 1
|
489 |
|
490 | def _ShTraceBegin(self):
|
491 | # type: () -> Optional[mylib.BufWriter]
|
492 | if not self.exec_opts.xtrace() or not self.exec_opts.xtrace_details():
|
493 | return None
|
494 |
|
495 | # Note: bash repeats the + for command sub, eval, source. Other shells
|
496 | # don't do it. Leave this out for now.
|
497 | prefix = self._EvalPS4('+')
|
498 | buf = mylib.BufWriter()
|
499 | buf.write(prefix)
|
500 | return buf
|
501 |
|
502 | def _RichTraceBegin(self, punct):
|
503 | # type: (str) -> Optional[mylib.BufWriter]
|
504 | """For the stack printed by xtrace_rich."""
|
505 | if not self.exec_opts.xtrace() or not self.exec_opts.xtrace_rich():
|
506 | return None
|
507 |
|
508 | prefix = self._EvalPS4(punct)
|
509 | buf = mylib.BufWriter()
|
510 | buf.write(prefix)
|
511 | return buf
|
512 |
|
513 | def OnProcessStart(self, pid, why):
|
514 | # type: (int, trace_t) -> None
|
515 | """
|
516 | In parent, Process::StartProcess calls us with child PID
|
517 | """
|
518 | UP_why = why
|
519 | with tagswitch(why) as case:
|
520 | if case(trace_e.External):
|
521 | why = cast(trace.External, UP_why)
|
522 |
|
523 | # There is the empty argv case of $(true), but it's never external
|
524 | assert len(why.argv) > 0
|
525 | self.multi_trace.EmitArgv0(why.argv[0])
|
526 |
|
527 | buf = self._RichTraceBegin('|')
|
528 | if not buf:
|
529 | return
|
530 |
|
531 | # TODO: ProcessSub and PipelinePart are commonly command.Simple, and also
|
532 | # Fork/ForkWait through the BraceGroup. We could print those argv arrays.
|
533 |
|
534 | with tagswitch(why) as case:
|
535 | # Synchronous cases
|
536 | if case(trace_e.External):
|
537 | why = cast(trace.External, UP_why)
|
538 | buf.write('command %d:' % pid)
|
539 | _PrintYshArgv(why.argv, buf)
|
540 |
|
541 | # Everything below is the same. Could use string literals?
|
542 | elif case(trace_e.ForkWait):
|
543 | buf.write('forkwait %d\n' % pid)
|
544 | elif case(trace_e.CommandSub):
|
545 | buf.write('command sub %d\n' % pid)
|
546 |
|
547 | # Async cases
|
548 | elif case(trace_e.ProcessSub):
|
549 | buf.write('proc sub %d\n' % pid)
|
550 | elif case(trace_e.HereDoc):
|
551 | buf.write('here doc %d\n' % pid)
|
552 | elif case(trace_e.Fork):
|
553 | buf.write('fork %d\n' % pid)
|
554 | elif case(trace_e.PipelinePart):
|
555 | buf.write('part %d\n' % pid)
|
556 |
|
557 | else:
|
558 | raise AssertionError()
|
559 |
|
560 | self.f.write(buf.getvalue())
|
561 |
|
562 | def OnProcessEnd(self, pid, status):
|
563 | # type: (int, int) -> None
|
564 | buf = self._RichTraceBegin(';')
|
565 | if not buf:
|
566 | return
|
567 |
|
568 | buf.write('process %d: status %d\n' % (pid, status))
|
569 | self.f.write(buf.getvalue())
|
570 |
|
571 | def OnNewProcess(self, child_pid):
|
572 | # type: (int) -> None
|
573 | """All trace lines have a PID prefix, except those from the root
|
574 | process."""
|
575 | self.val_pid_str.s = ' %d' % child_pid
|
576 | self._Inc()
|
577 | self.multi_trace.OnNewProcess(child_pid)
|
578 |
|
579 | def PushMessage(self, label, argv):
|
580 | # type: (str, Optional[List[str]]) -> None
|
581 | """For synchronous constructs that aren't processes."""
|
582 | buf = self._RichTraceBegin('>')
|
583 | if buf:
|
584 | buf.write(label)
|
585 | if label in ('proc', 'module-invoke'):
|
586 | _PrintYshArgv(argv, buf)
|
587 | elif label in ('source', 'use'):
|
588 | _PrintYshArgv(argv[1:], buf)
|
589 | elif label == 'wait':
|
590 | _PrintYshArgv(argv[1:], buf)
|
591 | else:
|
592 | buf.write('\n')
|
593 | self.f.write(buf.getvalue())
|
594 |
|
595 | self._Inc()
|
596 |
|
597 | def PopMessage(self, label, arg):
|
598 | # type: (str, Optional[str]) -> None
|
599 | """For synchronous constructs that aren't processes.
|
600 |
|
601 | e.g. source or proc
|
602 | """
|
603 | self._Dec()
|
604 |
|
605 | buf = self._RichTraceBegin('<')
|
606 | if buf:
|
607 | buf.write(label)
|
608 | if arg is not None:
|
609 | buf.write(' ')
|
610 | # TODO: use unquoted -> POSIX '' -> b''
|
611 | buf.write(j8_lite.MaybeShellEncode(arg))
|
612 | buf.write('\n')
|
613 | self.f.write(buf.getvalue())
|
614 |
|
615 | def OtherMessage(self, message):
|
616 | # type: (str) -> None
|
617 | """Can be used when receiving signals."""
|
618 | buf = self._RichTraceBegin('!')
|
619 | if not buf:
|
620 | return
|
621 |
|
622 | buf.write(message)
|
623 | buf.write('\n')
|
624 | self.f.write(buf.getvalue())
|
625 |
|
626 | def OnExec(self, argv):
|
627 | # type: (List[str]) -> None
|
628 | buf = self._RichTraceBegin('.')
|
629 | if not buf:
|
630 | return
|
631 | buf.write('exec')
|
632 | _PrintYshArgv(argv, buf)
|
633 | self.f.write(buf.getvalue())
|
634 |
|
635 | def OnBuiltin(self, builtin_id, argv):
|
636 | # type: (builtin_t, List[str]) -> None
|
637 | if builtin_id in (builtin_i.eval, builtin_i.source, builtin_i.use,
|
638 | builtin_i.wait):
|
639 | return # These builtins are handled separately
|
640 |
|
641 | buf = self._RichTraceBegin('.')
|
642 | if not buf:
|
643 | return
|
644 | buf.write('builtin')
|
645 | _PrintYshArgv(argv, buf)
|
646 | self.f.write(buf.getvalue())
|
647 |
|
648 | #
|
649 | # Shell Tracing That Begins with _ShTraceBegin
|
650 | #
|
651 |
|
652 | def OnSimpleCommand(self, argv):
|
653 | # type: (List[str]) -> None
|
654 | """For legacy set -x.
|
655 |
|
656 | Called before we know if it's a builtin, external, or proc.
|
657 | """
|
658 | buf = self._ShTraceBegin()
|
659 | if not buf:
|
660 | return
|
661 |
|
662 | # Redundant with OnProcessStart (external), PushMessage (proc), and OnBuiltin
|
663 | if self.exec_opts.xtrace_rich():
|
664 | return
|
665 |
|
666 | # Legacy: Use SHELL encoding, NOT _PrintYshArgv()
|
667 | PrintShellArgv(argv, buf)
|
668 | buf.write('\n')
|
669 | self.f.write(buf.getvalue())
|
670 |
|
671 | def OnAssignBuiltin(self, cmd_val):
|
672 | # type: (cmd_value.Assign) -> None
|
673 | buf = self._ShTraceBegin()
|
674 | if not buf:
|
675 | return
|
676 |
|
677 | for i, arg in enumerate(cmd_val.argv):
|
678 | if i != 0:
|
679 | buf.write(' ')
|
680 | buf.write(arg)
|
681 |
|
682 | for pair in cmd_val.pairs:
|
683 | buf.write(' ')
|
684 | buf.write(pair.var_name)
|
685 | buf.write('+=' if pair.plus_eq else '=')
|
686 | if pair.rval:
|
687 | _PrintShValue(pair.rval, buf)
|
688 |
|
689 | buf.write('\n')
|
690 | self.f.write(buf.getvalue())
|
691 |
|
692 | def OnShAssignment(self, lval, op, val, flags, which_scopes):
|
693 | # type: (sh_lvalue_t, assign_op_t, value_t, int, scope_t) -> None
|
694 | buf = self._ShTraceBegin()
|
695 | if not buf:
|
696 | return
|
697 |
|
698 | left = '?'
|
699 | UP_lval = lval
|
700 | with tagswitch(lval) as case:
|
701 | if case(sh_lvalue_e.Var):
|
702 | lval = cast(LeftName, UP_lval)
|
703 | left = lval.name
|
704 | elif case(sh_lvalue_e.Indexed):
|
705 | lval = cast(sh_lvalue.Indexed, UP_lval)
|
706 | left = '%s[%d]' % (lval.name, lval.index)
|
707 | elif case(sh_lvalue_e.Keyed):
|
708 | lval = cast(sh_lvalue.Keyed, UP_lval)
|
709 | left = '%s[%s]' % (lval.name, j8_lite.MaybeShellEncode(
|
710 | lval.key))
|
711 | buf.write(left)
|
712 |
|
713 | # Only two possibilities here
|
714 | buf.write('+=' if op == assign_op_e.PlusEqual else '=')
|
715 |
|
716 | _PrintShValue(val, buf)
|
717 |
|
718 | buf.write('\n')
|
719 | self.f.write(buf.getvalue())
|
720 |
|
721 | def OnControlFlow(self, keyword, arg):
|
722 | # type: (str, int) -> None
|
723 |
|
724 | # This is NOT affected by xtrace_rich or xtrace_details. Works in both.
|
725 | if not self.exec_opts.xtrace():
|
726 | return
|
727 |
|
728 | prefix = self._EvalPS4('+')
|
729 | buf = mylib.BufWriter()
|
730 | buf.write(prefix)
|
731 |
|
732 | buf.write(keyword)
|
733 | buf.write(' ')
|
734 | buf.write(str(arg)) # Note: 'return' is equivalent to 'return 0'
|
735 | buf.write('\n')
|
736 |
|
737 | self.f.write(buf.getvalue())
|
738 |
|
739 | def PrintSourceCode(self, left_tok, right_tok, arena):
|
740 | # type: (Token, Token, alloc.Arena) -> None
|
741 | """For (( )) and [[ ]].
|
742 |
|
743 | Bash traces these.
|
744 | """
|
745 | buf = self._ShTraceBegin()
|
746 | if not buf:
|
747 | return
|
748 |
|
749 | line = left_tok.line.content
|
750 | start = left_tok.col
|
751 |
|
752 | if left_tok.line == right_tok.line:
|
753 | end = right_tok.col + right_tok.length
|
754 | buf.write(line[start:end])
|
755 | else:
|
756 | # Print first line only
|
757 | end = -1 if line.endswith('\n') else len(line)
|
758 | buf.write(line[start:end])
|
759 | buf.write(' ...')
|
760 |
|
761 | buf.write('\n')
|
762 | self.f.write(buf.getvalue())
|