2 # Convenience shell for using sharkd, including history and tab completion.
4 # Copyright (c) 2019 Peter Wu <peter@lekensteyn.nl>
6 # SPDX-License-Identifier: GPL-2.0-or-later
20 _logger
= logging
.getLogger(__name__
)
22 # grep -Po 'tok_req, "\K\w+' sharkd_session.c
50 def __init__(self
, pretty
, history_file
):
52 self
.history_file
= history_file
54 def ignore_sigint(self
):
55 # Avoid terminating the sharkd child when ^C in the shell.
56 signal
.signal(signal
.SIGINT
, signal
.SIG_IGN
)
58 def sharkd_process(self
):
60 env
= os
.environ
.copy()
61 # Avoid loading user preferences which may trigger deprecation warnings.
62 env
['WIRESHARK_CONFIG_DIR'] = '/nonexistent'
63 proc
= subprocess
.Popen([sharkd
, '-'],
64 stdin
=subprocess
.PIPE
,
65 stdout
=subprocess
.PIPE
,
66 stderr
=subprocess
.PIPE
,
68 preexec_fn
=self
.ignore_sigint
)
69 banner
= proc
.stderr
.read1().decode('utf8')
70 if banner
.strip() != 'Hello in child.':
71 _logger
.warning('Unexpected banner: %r', banner
)
74 def completer(self
, text
, state
):
76 origline
= readline
.get_line_buffer()
77 line
= origline
.lstrip()
78 skipped
= len(origline
) - len(line
)
79 startpos
= readline
.get_begidx() - skipped
80 curpos
= readline
.get_endidx() - skipped
81 # _logger.debug('Completing: head=%r cur=%r tail=%r',
82 # line[:startpos], line[startpos:curpos], line[curpos:])
85 completions
= all_commands
87 cmd
= line
[1:startpos
].strip()
89 completions
= ['jq', 'indent', 'off']
90 elif cmd
== 'histfile':
91 # spaces in paths are not supported for now.
92 completions
= glob
.glob(glob
.escape(text
) + '*')
94 completions
= ['on', 'off']
95 completions
= [x
for x
in completions
if x
.startswith(text
)]
96 if len(completions
) == 1:
97 completions
= [completions
[0] + ' ']
98 self
.completions
= completions
100 return self
.completions
[state
]
104 def wrap_exceptions(self
, fn
):
105 # For debugging, any exception in the completion function is usually
106 # silently ignored by readline.
110 except Exception as e
:
115 def add_history(self
, line
):
116 # Emulate HISTCONTROL=ignorespace to avoid adding to history.
117 if line
.startswith(' '):
119 # Emulate HISTCONTROL=ignoredups to avoid duplicate history entries.
120 nitems
= readline
.get_current_history_length()
121 lastline
= readline
.get_history_item(nitems
)
123 readline
.add_history(line
)
125 def parse_command(self
, cmd
):
126 '''Converts a user-supplied command to a sharkd one.'''
127 # Support 'foo {...}' as alias for '{"req": "foo", ...}'
130 req
, cmd
= cmd
.split(' ', 1)
134 return self
.parse_special_command(cmd
[1:])
141 except json
.JSONDecodeError
as e
:
142 _logger
.error('Invalid command: %s', e
)
144 if type(c
) != dict or not 'req' in c
:
145 _logger
.error('Missing req key in request')
149 def parse_special_command(self
, cmd
):
152 _logger
.warning('Missing command')
154 if args
[0] == 'pretty':
155 choices
= ['jq', 'indent']
157 self
.pretty
= args
[1] if args
[1] in choices
else None
158 print('Pretty printing is now', self
.pretty
or 'disabled')
159 elif args
[0] == 'histfile':
161 self
.history_file
= args
[1] if args
[1] != 'off' else None
162 print('History is now', self
.history_file
or 'disabled')
163 elif args
[0] == 'debug':
164 if len(args
) >= 2 and args
[1] in ('on', 'off'):
166 logging
.DEBUG
if args
[1] == 'on' else logging
.INFO
)
167 print('Debug logging is now',
168 ['off', 'on'][_logger
.level
== logging
.DEBUG
])
170 _logger
.warning('Unsupported command %r', args
[0])
172 @contextlib.contextmanager
173 def wrap_history(self
):
174 '''Loads history at startup and saves history on exit.'''
175 readline
.set_auto_history(False)
177 if self
.history_file
:
178 readline
.read_history_file(self
.history_file
)
179 h_len
= readline
.get_current_history_length()
180 except FileNotFoundError
:
185 new_items
= readline
.get_current_history_length() - h_len
186 if new_items
> 0 and self
.history_file
:
187 open(self
.history_file
, 'a').close()
188 readline
.append_history_file(new_items
, self
.history_file
)
190 def shell_prompt(self
):
191 '''Sets up the interactive prompt.'''
192 readline
.parse_and_bind("tab: complete")
193 readline
.set_completer(self
.wrap_exceptions(self
.completer
))
194 readline
.set_completer_delims(' ')
195 return self
.wrap_history()
197 def read_command(self
):
200 origline
= input('# ')
203 except KeyboardInterrupt:
204 print('^C', file=sys
.stderr
)
206 cmd
= origline
.strip()
209 self
.add_history(origline
)
210 c
= self
.parse_command(cmd
)
214 def want_input(self
):
215 '''Request the prompt to be displayed.'''
216 os
.write(self
.user_input_wr
, b
'x')
219 sel
= selectors
.DefaultSelector()
220 user_input_rd
, self
.user_input_wr
= os
.pipe()
222 with self
.sharkd_process() as proc
, self
.shell_prompt():
224 sel
.register(proc
.stdout
, selectors
.EVENT_READ
, self
.handle_stdout
)
225 sel
.register(proc
.stderr
, selectors
.EVENT_READ
, self
.handle_stderr
)
226 sel
.register(user_input_rd
, selectors
.EVENT_READ
, self
.handle_user
)
230 events
= sel
.select()
231 _logger
.debug('got events: %r', events
)
234 for key
, mask
in events
:
237 except KeyboardInterrupt:
238 print('Interrupt again to abort immediately.', file=sys
.stderr
)
242 if self
.want_command
:
243 self
.ask_for_command_and_run_it()
244 # Process died? Stop the shell.
245 if proc
.poll() is not None:
248 def handle_user(self
, key
):
249 '''Received a notification that another prompt can be displayed.'''
250 os
.read(key
.fileobj
, 4096)
251 self
.want_command
= True
253 def ask_for_command_and_run_it(self
):
254 cmd
= self
.read_command()
256 # Give a chance for the event loop to run again.
259 self
.want_command
= False
260 _logger
.debug('Running: %r', cmd
)
261 self
.process
.stdin
.write((cmd
+ '\n').encode('utf8'))
262 self
.process
.stdin
.flush()
264 def handle_stdout(self
, key
):
265 resp
= key
.fileobj
.readline().decode('utf8')
266 _logger
.debug('Response: %r', resp
)
273 if self
.pretty
== 'jq':
274 subprocess
.run(['jq', '.'], input=resp
,
275 universal_newlines
=True)
276 elif self
.pretty
== 'indent':
278 json
.dump(r
, sys
.stdout
, indent
=' ')
282 except Exception as e
:
283 _logger
.warning('Dumping output as-is due to: %s', e
)
286 def handle_stderr(self
, key
):
287 data
= key
.fileobj
.read1().decode('utf8')
288 print(data
, end
="", file=sys
.stderr
)
291 parser
= argparse
.ArgumentParser()
292 parser
.add_argument('--debug', action
='store_true',
293 help='Enable verbose logging')
294 parser
.add_argument('--pretty', choices
=['jq', 'indent'],
295 help='Pretty print responses (one of: %(choices)s)')
296 parser
.add_argument('--histfile',
297 help='Log shell history to this file')
301 logging
.basicConfig()
302 _logger
.setLevel(logging
.DEBUG
if args
.debug
else logging
.INFO
)
303 shell
= SharkdShell(args
.pretty
, args
.histfile
)
310 if __name__
== '__main__':
311 main(parser
.parse_args())