diff --git a/.gitignore b/.gitignore index d9ce6ee..384cf33 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,15 @@ __pycache__/ # C lib *.so +# Visual Studio +*.sln +*.suo +*.pyproj +.vs/ + +# Vim +*.swp + # Distribution / packaging .Python env/ diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..deccd5f --- /dev/null +++ b/TODO.md @@ -0,0 +1,20 @@ +# Scimitar +## Ye Distributed Debugger + +### Pending merges: +* GDB/MI + * mi_parser (`8ec00bfed88d4beda1c37a516d953638`) +* Report PIDs from HPX + * hpx_pids (`4c2e6efda9334f50a97498ff3df4ca37`) +* AsyncIO + * asyncio_processing_loop (`939bad3d2718407e8b07176c14839ba0`) + * live_output (`b09de9acc7ad476fb09ce2dd4bd1ad69`) +* UI + * ui_prompt_toolkit (`26e3c2d2ae0239993eb5fe3fa015b126`) + * ui_qt (`a6774b4ac2646bf9cad9687bad92b087`) +* Pretty Printers + * natvis_transformer (`fecd531769f64374a7848815c9299e57`) +* Interaction with HPX Runtime + * pfx_counters (`a4aab1c4f49b48e396b0340924281c22`) + * ns_query (`1fea6b7c6da446538a35a98f263717fe`) + diff --git a/assets/archer-fish-icon.psd b/assets/archer-fish-icon.psd new file mode 100644 index 0000000..45657c2 Binary files /dev/null and b/assets/archer-fish-icon.psd differ diff --git a/assets/archer-fish.ico b/assets/archer-fish.ico new file mode 100644 index 0000000..b37db5f Binary files /dev/null and b/assets/archer-fish.ico differ diff --git a/assets/archer-fish.png b/assets/archer-fish.png new file mode 100644 index 0000000..4dc7882 Binary files /dev/null and b/assets/archer-fish.png differ diff --git a/assets/icon-2048.png b/assets/icon-2048.png new file mode 100644 index 0000000..2c40fbb Binary files /dev/null and b/assets/icon-2048.png differ diff --git a/assets/icon-256.png b/assets/icon-256.png new file mode 100644 index 0000000..9d85858 Binary files /dev/null and b/assets/icon-256.png differ diff --git a/assets/icon-32.png b/assets/icon-32.png new file mode 100644 index 0000000..b8d49ef Binary files /dev/null and b/assets/icon-32.png differ diff --git a/assets/logo-gnu-gbd-cleaned-up.png b/assets/logo-gnu-gbd-cleaned-up.png new file mode 100644 index 0000000..7ae9667 Binary files /dev/null and b/assets/logo-gnu-gbd-cleaned-up.png differ diff --git a/assets/logo-gnu-gbd-remastered.png b/assets/logo-gnu-gbd-remastered.png new file mode 100644 index 0000000..54ad269 Binary files /dev/null and b/assets/logo-gnu-gbd-remastered.png differ diff --git a/assets/logo-gnu-gbd-touched.png b/assets/logo-gnu-gbd-touched.png new file mode 100644 index 0000000..e9063fc Binary files /dev/null and b/assets/logo-gnu-gbd-touched.png differ diff --git a/assets/logo-gnu-gbd.png b/assets/logo-gnu-gbd.png new file mode 100644 index 0000000..eb926be Binary files /dev/null and b/assets/logo-gnu-gbd.png differ diff --git a/assets/logo-gnu-gbd.psd b/assets/logo-gnu-gbd.psd new file mode 100644 index 0000000..0de5525 Binary files /dev/null and b/assets/logo-gnu-gbd.psd differ diff --git a/assets/logo-gnu-gdb-archer.png b/assets/logo-gnu-gdb-archer.png new file mode 100644 index 0000000..3d8f853 Binary files /dev/null and b/assets/logo-gnu-gdb-archer.png differ diff --git a/readme.md b/readme.md index b61338b..fa5d75b 100644 --- a/readme.md +++ b/readme.md @@ -1,81 +1,8 @@ # Scimitar ## Ye Distributed Debugger -### GDB Integration -* The scripts are in the - [tools](`https://github.com/parsa/scimitar/tree/master/tools`) directory. -* To import the printers: - * If your GDB is set up to perform auto loading simply copy `auto-load` and - `python` directories to the appropriate locations. - * If you're not using auto-load then ensure the path to auto-load and - Python directories are in `sys.path` - * One option to add them to GDB Python's sys.path is running `python - sys.path.append(`''`)` for both directories. - * Run `python import scimitar_gdb` inside GDB - * You can also put the commands inside your `.gdbrc` +Scimitar is a distributed debugging tool for HPX applications. It is a front-end for GDB and adds features to make common operations such as switching between several sessions (like localities in HPX) and viewing HPX's internal data structures easier. -``` -python -sys.path.extend([ - '/auto-load', - '/python', -]) -import scimitar_gdb -end -``` +### Documentation +* For documentation consult [Scimitar's wiki pages](https://github.com/STEllAR-GROUP/scimitar/wiki) -### Prerequisites -* Software: - * Python 2.7 - * GDB 7.1 -* Python Modules - * pexpect - -### Configuration -In order to prevent having to enter the debugging environment configurations -every time it is launched and save time Scimitar uses the file -`utils/config.py` to retrieve the configurations of a cluster. You may modify -and add to it to meet your needs. - -### Running -* Schedule a job to run your application. Ensure mpirun starts. -* Run `scimitar.py` on your machine -* Start a session by `remote ` -* Once you're connected you can switch between localities by using the command - `switch ` - -## Commands -* local raw -* local [ ...] -* local ls -* local ls -* remote -* remote -* remote attach [ ...] - -### Pending merges: -* GDB/MI - * mi_parser (`8ec00bfed88d4beda1c37a516d953638`) -* Sessions - * local_session (`8c110db273af4a81bea68ef8686f1beb`) - * switch_locality (`6d52ba7248ed48368d556620d753cbce`) -* Report PIDs from HPX - * hpx_pids (`4c2e6efda9334f50a97498ff3df4ca37`) -* AsyncIO - * asyncio_processing_loop (`939bad3d2718407e8b07176c14839ba0`) - * live_output (`b09de9acc7ad476fb09ce2dd4bd1ad69`) -* UI - * ui_wxwidgets (`f49ea035cbc845099ac8356d9147dfb0`) - * ui_curses (`c68045350edc449a90b1dbc4ddbeeb08`) -* Pretty Printers - * natvis_transformer (`fecd531769f64374a7848815c9299e57`) -* config.py - * dotsshconfig (`a6206aa120844233b986cb470013cf54`) - * stampede_config (`3c21aec9daba4bc49fd2d0d98ec0e46b`) - * edison_config (`613a076ab3254014b55f645a7d85e529`) - * cori_config (`d8459d9a002047239fb21c3c92050980`) - * bigdat_config (`406ec14fae894e66ad147245ede1abda`) - * supermike2_config (`08e71a6fd99246c7ad01e996dd79fea2`) -* Interaction with HPX Runtime - * pfx_counters (`a4aab1c4f49b48e396b0340924281c22`) - * ns_query (`1fea6b7c6da446538a35a98f263717fe`) diff --git a/scimitar.py b/scimitar.py deleted file mode 100755 index b53a61b..0000000 --- a/scimitar.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Scimitar: Ye Distributed Debugger -# -# Copyright (c) 2016 Parsa Amini -# Copyright (c) 2016 Hartmut Kaiser -# Copyright (c) 2016 Thomas Heller -# -# Distributed under the Boost Software License, Version 1.0. (See accompanying -# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -# -import signal -from sys import stdout -import time -import thread -import threading -from util import config, vt100, print_out, print_ahead, print_error, raw_input_async, repr_str -import session - -# Constants -BANNER = ''' - 7?$7: - ____ _ _ _ +DDO7I~+. - / ___| ___(_)_ __ ___ (_) |_ __ _ _ __ .I: .NDDOZ?. ... - \___ \ / __| | '_ ` _ \| | __/ _` | '__| .+~ DDD8Z+. - ___) | (__| | | | | | | | || (_| | | .7?IDD8Z+. - |____/ \___|_|_| |_| |_|_|\__\__,_|_| (alpha) .$?I+=$=. - .?77$=+..?. - 0.3.193 build 3109 .III77777,.:I. - .~?????I,?. .~. - ..?++++?=?+. - .?+++++I 7: - .+????+I.$+. - .+?????I,7+. - ..:=+++++?I:7=. -+. . ,~~~===++++,$=. - .:77$7777I?~~++=?IIII?++++=====~~~~~~=~.$?, - .=777777III????????+++?=:?~=~~,.?I~.. - ,+77I????++++++=+++=,,I7+:. - . ..,:~====~::,. - -Copyright (C) 2016 Parsa Amini -Copyright (C) 2016 Hartmut Kaiser -Copyright (C) 2016 Thomas Heller - -Be licensed under Boost Software License, Version 1.0 - -'tis be free software; ye be free to change 'n redistribute it. Thar be NO -warranty; not even for MERCHANTABILITY or FITNESS FER A PARTICULAR PURPOSE. -''' - - -# HACK: Test async output printing -def noise(): - pass - #print_ahead('Noise.', config.settings['ui']['prompt']) -# for _ in range(20): -# time.sleep(4) -# -# print_ahead('Noise.', config.settings['ui']['prompt']) - -# Dispatch the command and its arguments to the appropriate mode's processor - - -command_handler_switcher = { - session.modes.offline: session.offline.process, - session.modes.debugging: session.debugging.process, -} - - -def main(): - # Clear the terminal - vt100.terminal.reset() - # Ahoy - print_out(BANNER) - - # Initial session mode - state = session.modes.offline - - # Async output printing - thread.start_new_thread(noise, ()) - - # Main loop - while state != session.modes.quit: - vt100.unlock_keyboard() - # FIXME: raw_input_async still is a blocking call. Have found no way to - # avoid it. Reason: readline initiates a system call that I have no - # clue to get out of. - user_input, key_seq = raw_input_async(config.settings['ui']['prompt']) - # HACK: Temporarily disabled for debugging - #vt100.lock_keyboard() - ## HACK: Display the user's input - #print_out(user_input.encode('string_escape') if user_input else '') - - # An empty string is a valid empty - # If the input was a control signal split might just remove it - packed_input = user_input if user_input else key_seq - cmd, args = packed_input[0], packed_input[1:] - # Run the appropriate mode's processing function - cmd_processor_fn = command_handler_switcher.get(state) - - try: - state, update_msg = cmd_processor_fn(cmd, args) - if update_msg: - print_out(update_msg) - except session.UnknownCommandError as e: - print_error( - 'Unknown command: {u1}{cmd}{u0}', cmd = repr_str(e.expression) - ) - except session.BadArgsError as e: - print_error( - 'Command "{u1}{cmd}{u0}" cannot be initiated with the arguments provided.\n{msg}', - cmd = e.expression, - msg = e.message - ) - except session.BadConfigError as e: - print_error( - 'The command encountered errors with the provided arguments.\n{u1}{cmd}{u0}: {msg}.', - cmd = e.expression, - msg = e.message - ) - except session.CommandFailedError as e: - print_error( - 'The command encountered an error and did not run properly.\n{u1}{cmd}{u0}: {msg}.', - cmd = e.expression, - msg = e.message - ) - except session.CommandImplementationIncompleteError: - print_error( - 'The implementation of command "{u1}{cmd}{u0}" is not complete yet.', - cmd = cmd - ) - except KeyboardInterrupt: - print_error('Action cancelled by the user.') - - -if __name__ == '__main__': - # NOTE: Multiple SIGKILLs required to force close. - raw_input_async.last_kill_sig = None - try: - main() - finally: - # Clean up the terminal before letting go - vt100.unlock_keyboard() - vt100.format.clear_all_chars_attrs() - -# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/LICENSE_1_0.txt b/scimitar/LICENSE_1_0.txt similarity index 100% rename from LICENSE_1_0.txt rename to scimitar/LICENSE_1_0.txt diff --git a/scimitar/__init__.py b/scimitar/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/util/exceptions.py b/scimitar/__ver__.py similarity index 90% rename from util/exceptions.py rename to scimitar/__ver__.py index 424d346..9cfd4d3 100644 --- a/util/exceptions.py +++ b/scimitar/__ver__.py @@ -9,7 +9,7 @@ # Distributed under the Boost Software License, Version 1.0. (See accompanying # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # -class ScimitarError(Exception): - pass + +VERSION = '0.3.203 build 3494' # vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/command_completer.py b/scimitar/command_completer.py new file mode 100644 index 0000000..9924f01 --- /dev/null +++ b/scimitar/command_completer.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016-2017 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +from prompt_toolkit.completion import Completer, Completion + + +class CommandCompleter(Completer): + + def __init__(self): + self.current_candidates = [] + + def _complete_command(self): + raise NotImplementedError('Must override _complete_command') + + def _complete_command_arguments(self, command, words): + raise NotImplementedError('Must override _complete_command_arguments') + + def _prune_nonmatches(self, candidates, being_completed): + if being_completed: + result = [ + candidate for candidate in candidates + if candidate.startswith(being_completed) + ] + else: + result = candidates + + # If it's the only choice + if result and len(result) == 1: + return [result[0] + ' '] + return result + + def get_completions(self, document, complete_event): + last_word = document.get_word_before_cursor() + + # First token + if not document.current_line or not ' ' in document.current_line: + candidates = self._complete_command() + else: + words = document.current_line.split() + + candidates = self._complete_command_arguments( + words[0], words[1:] + ) + + matches = self._prune_nonmatches( + candidates, last_word + ) + + if not matches: + return [] + return [Completion(i, start_position=-len(last_word)) for i in matches] + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/config.py b/scimitar/config.py new file mode 100644 index 0000000..e2289e5 --- /dev/null +++ b/scimitar/config.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016-2017 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +from __future__ import unicode_literals + +settings = { + 'ui': { + 'prompts': [ + '# ', # Level 0: Offline mode + '$ ', # Level 1: Debugging mode + ], + }, + 'signals': { + 'sigkill': + 5, + 'sigkill_last': + 1, + # TODO: See if we need to handle the other signals. These maybe: + ## EOF + ## SIGALRM + ## SIGINT + ## SIGQUIT + ## SIGTERM + ## SIGSTOP + }, + 'gdb': { + # GDB command line + # Supress banner, interactive mode + 'cmd': [ + 'gdb', + '-interpreter=mi2', # Use GDB/MI2 interface + '-quiet', # Suppress banner + '--nx', # Don't load any .gdbinits whatsoever + ], + 'attach': + '--pid={pid}', + }, + 'sessions': { + 'history_length': 100 + }, +} + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/console.py b/scimitar/console.py new file mode 100644 index 0000000..624e1a6 --- /dev/null +++ b/scimitar/console.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +import re +import errors +import pexpect + + +class SessionDiedError(errors.ScimitarError): + "Raised when attempting to modify hops while there are active sessions." + pass + + +class NoHopsError(errors.ScimitarError): + "Rased when there are no more hops to remove." + pass + + +class HopManager(object): + + def __init__(self): + self._hops = None + + def add(self, hop): + if not self._hops: + self._hops = [] + self._hops.append(hop) + + def list_hops(self): + return self._hops or [] + + def remove_last(self): + if not self._hops: + raise NoHopsError + return self._hops.pop() + + def is_empty(self): + return not self._hops + + +class SessionManager(object): + + def __init__(self): + self._session_dict = None + self._order = None + + def add(self, session): + if not self._session_dict: + self._session_dict = {} + self._order = [] + self._order.append(session) + self._session_dict[session.tag] = session + + def remove(self, session_tags): + for tag in session_tags: + session = self._session_dict.pop(tag) + self._order.remove(session) + + def list_sessions(self): + if not self._session_dict: + return [] + return self._session_dict.values() + + def list_session_tags(self): + if not self._session_dict: + return [] + return self._session_dict.keys() + + def exists(self, tag): + if self._session_dict and self._session_dict.has_key(tag): + return True + return False + + def get(self, tag): + return self._session_dict[tag] + + def is_empty(self): + return not self._session_dict + + def kill_all(self): + if self._session_dict: + for s in self._session_dict.itervalues(): + s.close() + self._session_dict = None + self._order = None + + def get_oldest(self): + if not self._order: + return None + return self._order[0] + + def get_newest(self): + if not self._order: + return None + return self._order[-1] + + +class Terminal(object): + ps1_export_cmd = r"export PS1='SCIMITAR_PS\n$ '" + ps1_re = r'SCIMITAR_PS\s+\$ ' + + def __init__( + self, + hops, + target_host = None, + meta = None, + tag = None, + exit_re = None, + prompt_re = None, + ): + self.con = None + self.hops = hops + + self.hostname = 'localhost' + + self.target_host = target_host + self.meta = meta + self.tag = tag + + self.exit_re = exit_re + self.prompt_re = prompt_re + + def __enter__(self): + return self.connect() + + def connect(self): + self.con = pexpect.spawn('/usr/bin/env bash') + + for hop in self.hops: + self.con.sendline('ssh -tt {host}'.format(host = hop)) + self.hostname = hop + + if self.target_host: + self.con.sendline('ssh -tt {host}'.format(host = self.target_host)) + self.hostname = self.target_host + + self.con.sendline(self.ps1_export_cmd) + self.con.expect(self.ps1_re) + + #all_sessions.append(self) + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + try: + cntr = 5 + while self.con.isalive() and cntr > 0: + self.query('quit') + cntr -= 1 + finally: + self.con.close() + + #all_sessions.remove(self) + + def query(self, cmd): + if not self.con.isalive(): + raise errors.DeadConsoleError + self.con.sendline(cmd) + try: + p_re = [self.ps1_re] + if self.exit_re: + p_re.insert(0, self.exit_re) + if self.prompt_re: + p_re.insert(0, self.prompt_re) + + pattern_index = self.con.expect(p_re) + if pattern_index == 0: + return self.con.before + elif pattern_index == 1: + self.close() + return '^exit' + elif pattern_index == 2: + self.con.close() + return '^kill' + except (pexpect.TIMEOUT, pexpect.EOF): + ## Connection's probably dead, close the socket + self.close() + raise errors.ConsoleSessionError + raise errors.UnexpectedResponseError + + def test_query(self, cmd): + if re.match( + '^.*aye[\r\n]*$', + self.query( + '{cmd} >/dev/null 2>&1 && echo aye || echo nay'. + format(cmd = cmd) + ), + re.DOTALL + ): + return True + return False + + def is_pid_alive(self, process_id): + """Checks if a PID is still valid + + :pid: The Process ID + :returns: bool + + """ + return self.test_query( + 'ps -p {pid}'.format(pid = process_id), re.DOTALL + ) + + def is_alive(self): + return self.con.isalive() + + def __repr__(self): + return ''.format( + self.tag, self.hostname, self.meta + ) + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/session/exceptions.py b/scimitar/errors.py similarity index 84% rename from session/exceptions.py rename to scimitar/errors.py index 766228b..998d06c 100644 --- a/session/exceptions.py +++ b/scimitar/errors.py @@ -9,7 +9,10 @@ # Distributed under the Boost Software License, Version 1.0. (See accompanying # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # -from util.exceptions import * + + +class ScimitarError(Exception): + pass class UnknownCommandError(ScimitarError): @@ -54,13 +57,18 @@ class UnexpectedResponseError(ScimitarError): pass -class NoRunningAppFoundError(ScimitarError): - '''Raised when no running application is found on the system''' +class CommandImplementationIncompleteError(ScimitarError): + '''Raised when a command I had to remove is called.''' + pass + + +class DeadConsoleError(ScimitarError): + '''Raised when encountering an unexpected response during terminal sessions.''' pass -class CommandImplementationIncompleteError(ScimitarError): - '''Raised when a command I had to remove is called.''' +class ConsoleSessionError(ScimitarError): + '''Raised when encountering an unexpected response during terminal sessions.''' pass # vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/mi_interface.py b/scimitar/mi_interface.py new file mode 100644 index 0000000..cbefc33 --- /dev/null +++ b/scimitar/mi_interface.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +import re + +_result_pattern = re.compile( + r'\^(?:(done)(?:,([^\r\n]+))?|(running)|(connected)|(error),msg="((?:\\.|[^"\\])+)"(?:,code="((?:\\.|[^"\\])+))?|(exit))' +) +_stream_pattern = re.compile(r'([~@&])"((?:\\.|[^\"\\])+)"') + +#_async_pattern = re.compile(r'') + + +def _safe_unescape(msg): + if type(msg) is str: + return msg.decode('string_escape') + return '' + + +def _safe_join_unescape(msg_col): + if msg_col: + return _safe_unescape(''.join(msg_col)) + return '' + + +def parse(output_records): + result_records = re.findall(_result_pattern, output_records) + stream_records = re.findall(_stream_pattern, output_records) + + result_indicator = None + if result_records: + result_record = result_records[0] + result_indicator_regex = next(( + result_record[index] for index in (0, 2, 3, 4, 7) + if result_record[index] + ), None) + + result_indicator = { + 'done': (indicator_done, _safe_unescape(result_record[1])), + 'running': (indicator_running, ), + 'connected': (indicator_connected, ), + 'error': ( + indicator_error, _safe_unescape(result_record[5]), + _safe_unescape(result_record[6]) + ), + 'exit': (indicator_exit, ), + }[result_indicator_regex] + + console_list, target_list, log_list = [], [], [] + for stream in stream_records: + { + '~': console_list, + '@': target_list, + '&': log_list, + }[stream[0]].append(stream[1]) + + console_out = _safe_join_unescape(console_list) + target_out = _safe_join_unescape(target_list) + log_out = _safe_join_unescape(log_list) + + return result_indicator, console_out, target_out, log_out + + +class indicator_done: + pass + + +class indicator_running: + pass + + +class indicator_connected: + pass + + +class indicator_error: + pass + + +class indicator_exit: + pass + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/modes.py b/scimitar/modes.py new file mode 100644 index 0000000..ecf86b3 --- /dev/null +++ b/scimitar/modes.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +class offline(object): + pass + + +class debugging(object): + pass + + +class quit(object): + pass + + # vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/session/modes.py b/scimitar/schedulers/__init__.py similarity index 87% rename from session/modes.py rename to scimitar/schedulers/__init__.py index e39c6d4..fe8a737 100644 --- a/session/modes.py +++ b/scimitar/schedulers/__init__.py @@ -9,7 +9,5 @@ # Distributed under the Boost Software License, Version 1.0. (See accompanying # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # -class modes: - offline, debugging, quit = range(3) # vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/schedulers/investigator.py b/scimitar/schedulers/investigator.py new file mode 100644 index 0000000..8bb53b3 --- /dev/null +++ b/scimitar/schedulers/investigator.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +import re +import errors +import pexpect +import console +import pbs +import slurm + + +class slurm_type: + pass + + +class pbs_type: + pass + + +scheduler = None + + +class MoreThanOneActiveJobError(errors.ScimitarError): + '''Raised when more than one active job is found.''' + pass + + +class NoActiveJobError(errors.ScimitarError): + '''Raised when no active job is found on the system''' + pass + + +class NoSchedulerFoundError(errors.ScimitarError): + '''Raised when the type of scheduler cannot be determined.''' + pass + + +class InvalidJobError(errors.ScimitarError): + '''Raised when the job id doesn't seem to be valid.''' + pass + + +class NoRunningAppFoundError(errors.ScimitarError): + '''Raised when no running application is found on the system''' + pass + + +def which_scheduler(term): + '''Tries to determine if the target system has a batch scheduling system we + can work with. + ''' + if term.test_query('type squeue'): + return slurm_type + if term.test_query('type qstat'): + return pbs_type + return None + + +def detect_scheduler(term): + '''Makes sure the target system does have a scheduler we can work with.''' + global scheduler + # Choose the right scheduler module + scheduler = { + slurm_type: slurm, + pbs_type: pbs, + None: None, + }[which_scheduler(term)] + # This test fails if there is no scheduler + if not scheduler: + raise NoSchedulerFoundError + return scheduler + + +def ls_user_jobs(term): + '''List this user's jobs + ''' + # Basic checks + if not scheduler: + raise NoSchedulerFoundError + return scheduler.ls_user_jobs(term) + + +def detect_active_job(term): + '''Tries to find the only active job. + ''' + # Basic checks + if not scheduler: + raise NoSchedulerFoundError + # Get job information + user_jobs = ls_user_jobs(term) + # There is more than one job + if len(user_jobs) > 1: + raise MoreThanOneActiveJobError + # Return the only active job, fail if there is none + try: + return next(iter(user_jobs or [])) + except StopIteration: + raise NoActiveJobError + + +def ls_job_pids(term, job_id, job_app = None): + '''Return the hosts in this job along with PIDs of job_app on each host + ''' + job_nodes = scheduler.ls_job_nodes(term, job_id) + + if len(job_nodes) == 0: + raise InvalidJobError + + # See if we can determine the app if none is provided + if not job_app: + node_0 = job_nodes[0] + try: + job_app = scheduler.which_appname(term, node_0) + except (IndexError, AttributeError): + raise NoRunningAppFoundError + + # Query each host for job_app + pid_dict = {} + for node in job_nodes: + pids = scheduler.ls_pids(term, node, job_app) + pid_dict[node] = pids + + return pid_dict + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/schedulers/pbs.py b/scimitar/schedulers/pbs.py new file mode 100644 index 0000000..c5ae7ae --- /dev/null +++ b/scimitar/schedulers/pbs.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +import re +import errors +import pexpect +import console + + +def ls_user_jobs(term): + cmd_out = term.query(r'qstat -u $USER') + return re.findall(r'^\d([^\s]+)', cmd_out) + + +def ls_job_nodes(term, job_id): + cmd_out = term.query(r'checkjob {job_id}'.format(job_id = job_id)) + return re.findall(r'\[(\w+):\d+\]', cmd_out) + + +def which_appname(term, host): + cmd_out = term.query( + r'''ssh {host} "ps -o pid:1,cmd:1 -e" | grep -o "MPISPAWN_ARGV_[0-9]='.\+'"'''. + format(host = host) + ) + return re.findall(r'MPISPAWN_ARGV_0=([\S]+)', cmd_out + )[0].replace(r'"', r'').replace(r"'", r'') + + +def ls_pids(term, host, appname): + cmd_out = term.query( + r'ssh {host} "pgrep {appname}"'.format( + host = host, appname = appname + ) + ) + return [int(pid) for pid in cmd_out.split()] + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/schedulers/slurm.py b/scimitar/schedulers/slurm.py new file mode 100644 index 0000000..7464ace --- /dev/null +++ b/scimitar/schedulers/slurm.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +import re +import errors +import pexpect +import console + +delimited_nodes_re = re.compile(r'\w+(?:\[\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*\])?') +grouped_nodes_re = re.compile(r'([^[]+)\[([^]]+)\]') + + +def _ungroup_node_names(job_nodes): + squeue_out = [] + + for node_subgroup in delimited_nodes_re.findall(job_nodes): + grouped_nodes = grouped_nodes_re.match(node_subgroup) + if grouped_nodes: + ungrouped_nodes = [] + base_name = grouped_nodes.group(1) + for id_parts in grouped_nodes.group(2).split(','): + range_parts = id_parts.split('-') + if len(range_parts) == 1: + ungrouped_nodes.append(base_name + range_parts[0]) + squeue_out += [base_name + range_parts[0]] + elif len(range_parts) == 2: + naming_length = len(range_parts[0]) + node_range_low = int(range_parts[0]) + node_range_up = int(range_parts[1]) + 1 + for node_number in range(node_range_low, node_range_up): + node_name = base_name + str(node_number).rjust( + naming_length, '0' + ) + ungrouped_nodes.append(base_name + range_parts[0]) + squeue_out += [node_name] + else: + squeue_out += [node_subgroup] + return squeue_out + + +def _sanitize_output(expr): + if not expr: + return '' + expr = expr.replace('\r', '') + expr = re.sub(r'\n{2,}', '\n', expr) + expr = re.sub(r'^[^\n]*\n', '', expr) + return re.sub(r'\n$', '', expr) + + +def ls_user_jobs(term): + query_cmd = '''squeue -h -o '%A' -u $USER''' + cmd_out = term.query(query_cmd) + cmd_out_san = _sanitize_output(cmd_out) + jobs = list(filter(None, cmd_out_san.split('\n'))) + return jobs + + +def ls_job_nodes(term, job_id): + query_cmd = '''squeue -h -o '%N' -j {job_id}'''.format(job_id = job_id) + cmd_out = term.query(query_cmd) + cmd_out_san = _sanitize_output(cmd_out) + + job_nodes = [] + if not 'Invalid' in cmd_out_san: + job_nodes = _ungroup_node_names(cmd_out_san) + return job_nodes + + +def which_appname(term, host): + query_cmd = '''ssh {host} "ps -o pid:1,cmd:1 -e" | grep -o "MPISPAWN_ARGV_[0-9]='.\+'"'''.format( + host = host + ) + cmd_out = term.query(query_cmd) + re_m = re.search('''MPISPAWN_ARGV_0=([\S]+)''', cmd_out).group(1) + appname = re_m.replace('"', '').replace("'", '') + return appname + + +# This nasty piece of work wasted so much of my time. Be ware: +# 'ssh marvin00 "pgrep mpi_hello_world"\r\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\r\r\n@ WARNING: POSSIBLE DNS SPOOFING DETECTED! @\r\r\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\r\r\nThe ECDSA host key for marvin00 has changed,\r\r\nand the key for the corresponding IP address 10.3.3.50\r\r\nis unknown. This could either mean that\r\r\nDNS SPOOFING is happening or the IP address for the host\r\r\nand its host key have changed at the same time.\r\r\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\r\r\n@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\r\r\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\r\r\nIT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\r\r\nSomeone could be eavesdropping on you right now (man-in-the-middle attack)!\r\r\nIt is also possible that a host key has just been changed.\r\r\nThe fingerprint for the ECDSA key sent by the remote host is\r\na9:18:47:c2:60:96:80:dc:a5:d3:bd:e7:7f:c1:3b:dd.\r\r\nPlease contact your system administrator.\r\r\nAdd correct host key in /home/pamini/.ssh/known_hosts to get rid of this message.\r\r\nOffending ECDSA key in /home/pamini/.ssh/known_hosts:16\r\r\nPassword authentication is disabled to avoid man-in-the-middle attacks.\r\r\nKeyboard-interactive authentication is disabled to avoid man-in-the-middle attacks.\r\r\n29177\r\n' +def ls_pids(term, host, appname): + cmd_out = term.query( + '''ssh {host} "pgrep {appname}"'''.format( + host = host, appname = appname + ) + ) + pids_m = re.search(r'\s*([\d\s]+)\s*$', cmd_out) + if not pids_m: + return [] + raw_pids = pids_m.group(1).split('\n') + raw_pids = list(filter(None, raw_pids)) + + return [int(pid) for pid in raw_pids] + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/scimitar.py b/scimitar/scimitar.py new file mode 100755 index 0000000..78103d3 --- /dev/null +++ b/scimitar/scimitar.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016-2017 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +from __future__ import unicode_literals +#import signal +#from sys import stdout +#import time +import thread +#import threading + +#from util import print_ahead +from util import vt100, print_out, print_error, raw_input_async, repr_str, cleanup_terminal, init_terminal, register_completer +import prompt_toolkit as ptk +from __ver__ import VERSION +from sessions import modes, offline_session, debug_session +import config +import errors + +# Constants +BANNER = ''' + 7?$7: + ____ _ _ _ +DDO7I~+. + / ___| ___(_)_ __ ___ (_) |_ __ _ _ __ .I: .NDDOZ?. ... + \___ \ / __| | '_ ` _ \| | __/ _` | '__| .+~ DDD8Z+. + ___) | (__| | | | | | | | || (_| | | .7?IDD8Z+. + |____/ \___|_|_| |_| |_|_|\__\__,_|_| (alpha) .$?I+=$=. + .?77$=+..?. + {get_version_result} .III77777,.:I. + .~?????I,?. .~. + ..?++++?=?+. + .?+++++I 7: + .+????+I.$+. + .+?????I,7+. + ..:=+++++?I:7=. ++. . ,~~~===++++,$=. + .:77$7777I?~~++=?IIII?++++=====~~~~~~=~.$?, + .=777777III????????+++?=:?~=~~,.?I~.. + ,+77I????++++++=+++=,,I7+:. + . ..,:~====~::,. + +Copyright (C) 2016 Parsa Amini +Copyright (C) 2016 Hartmut Kaiser +Copyright (C) 2016 Thomas Heller + +Be licensed under Boost Software License, Version 1.0 + +'tis be free software; ye be free to change 'n redistribute it. Thar be NO +warranty; not even for MERCHANTABILITY or FITNESS FER A PARTICULAR PURPOSE. +'''.format(get_version_result = VERSION.rjust(20)) + + +# HACK: Test async output printing +def noise(): + pass + #print_ahead('Noise.', config.settings['ui']['prompt']) +# for _ in range(20): +# time.sleep(4) +# +# print_ahead('Noise.', config.settings['ui']['prompt']) + +# Dispatch the command and its arguments to the appropriate mode's processor + + +completer_switcher = { + modes.offline: offline_session.OfflineSessionCommandCompleter(), + modes.debugging: debug_session.DebugSessionCommandCompleter(), + modes.quit: None, +} + +command_handler_switcher = { + modes.offline: offline_session.process, + modes.debugging: debug_session.process, +} + + +def main(): + try: + # NOTE: Multiple SIGKILLs required to force close. + raw_input_async.last_kill_sig = None + + # Clear the terminal + vt100.terminal.reset() + + # Ahoy + print(BANNER) + + # Initial session mode + current_mode = modes.offline + + # Init terminal + init_terminal() + register_completer(completer_switcher[current_mode]) + + # Async output printing + thread.start_new_thread(noise, ()) + + prompts_list = config.settings['ui']['prompts'] + get_prompt = lambda: { + modes.offline: prompts_list[0], + modes.debugging: prompts_list[1] + }[current_mode] + + # Main loop + while current_mode != modes.quit: + vt100.unlock_keyboard() + # FIXME: raw_input_async still is a blocking call. Have found no way to + # avoid it. Reason: readline initiates a system call that I have no + # clue to get out of. + user_input, key_seq = raw_input_async(get_prompt()) + # HACK: Temporarily disabled for debugging + #vt100.lock_keyboard() + ## HACK: Display the user's input + #print_out(user_input.encode('string_escape') if user_input else '') + + # An empty string is a valid empty + # If the input was a control signal split might just remove it + packed_input = user_input if user_input else key_seq + if not packed_input: + continue + cmd, args = packed_input[0], packed_input[1:] + # Run the appropriate mode's processing function + cmd_processor_fn = command_handler_switcher.get(current_mode) + + try: + current_mode, update_msg = cmd_processor_fn(cmd, args) + register_completer(completer_switcher[current_mode]) + + if update_msg: + print_out(update_msg) + except errors.UnknownCommandError as e: + print_error( + 'Unknown command: {u1}{cmd}{u0}', + cmd = repr_str(e.expression) + ) + except errors.BadArgsError as e: + print_error( + 'Command "{u1}{cmd}{u0}" cannot be initiated with the arguments provided.\n{msg}', + cmd = e.expression, + msg = e.message + ) + except errors.BadConfigError as e: + print_error( + 'The command encountered errors with the provided arguments.\n{u1}{cmd}{u0}: {msg}', + cmd = e.expression, + msg = e.message + ) + except errors.CommandFailedError as e: + print_error( + 'The command encountered an error and did not run properly.\n{u1}{cmd}{u0}: {msg}', + cmd = e.expression, + msg = e.message + ) + except errors.CommandImplementationIncompleteError: + print_error( + 'The implementation of command "{u1}{cmd}{u0}" is not complete yet.', + cmd = cmd + ) + except KeyboardInterrupt: + print_error('Action cancelled by the user.') + finally: + cleanup_terminal() + + +if __name__ == '__main__': + main() + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/session/__init__.py b/scimitar/sessions/__init__.py similarity index 64% rename from session/__init__.py rename to scimitar/sessions/__init__.py index c345f49..85d7f01 100644 --- a/session/__init__.py +++ b/scimitar/sessions/__init__.py @@ -9,12 +9,6 @@ # Distributed under the Boost Software License, Version 1.0. (See accompanying # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # -from .modes import * -from .exceptions import * - -from . import offline_session as offline -from . import local_session as local -from . import remote_session as remote -from . import debugging_session as debugging +import modes, offline_session, debug_session # vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/sessions/debug_session.py b/scimitar/sessions/debug_session.py new file mode 100644 index 0000000..ecebdee --- /dev/null +++ b/scimitar/sessions/debug_session.py @@ -0,0 +1,388 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +from collections import deque +import threading + +import errors +import modes +import console +import mi_interface as mi +from config import settings +from command_completer import CommandCompleter +from util import format_error, format_warning, format_info, repr_str_dict + +session_manager = None +selected_sessions = None +sessions_history = None + +history_length = int(settings['sessions']['history_length']) + + +def init_debugging_mode(mgr, msgs): + global selected_sessions + global session_manager + global sessions_history + + session_manager = mgr + sessions_history = {} + + oldest_session = mgr.get_oldest() + if oldest_session: + selected_sessions = [oldest_session.tag] + + for tag in session_manager.list_session_tags(): + sessions_history[tag] = deque(maxlen = history_length) + sessions_history[tag].append(msgs[tag]) + + return modes.debugging, None + + +def _ls_out(): + ls_out = [] + for s_i in session_manager.list_sessions(): + if not s_i.tag in selected_sessions: + ls_head = s_i.tag + else: + ls_head = '(*) {}'.format(s_i.tag) + ls_out.append('%6s) %s:%d' % ( + ls_head, + s_i.hostname, + s_i.meta, + )) + return '\n'.join(ls_out) + + +def list_sessions(args): + '''Lists all active sessions.''' + # Verify command syntax + if len(args) != 0: + raise errors.BadArgsError( + 'ls', 'This command does not accept arguments' + ) + + pretty_session_list = _ls_out() + if not pretty_session_list: + return modes.debugging, 'ls: Empty. No sessions are alive.' + return modes.debugging, 'Sessions:\n' + pretty_session_list + + +def select_session_complete(args): + '''readline library tab completion for session selection in debugging + mode''' + if not args: + return ['all', 'none'] + session_manager.list_session_tags() + + # If 'all' or 'none' then no further arguments are meaningful + if 'all' in args or 'none' in args: + return [] + + if 'all'.startswith(args[0]): + return ['all'] + + if 'none'.startswith(args[0]): + return ['none'] + + return [ + tag for tag in session_manager.list_session_tags() if not tag in args + ] + + +def select_session(args): + '''Mark the provided sessions as selected''' + # Verify command syntax + if not args: + raise errors.BadArgsError( + 'select', 'select all | none | [ [ ...]]' + ) + + if args: + if args[0] == 'all': + global selected_sessions + selected_sessions = sorted(session_manager.list_session_tags()) + + return modes.debugging, 'Selected session(s) #{0}\n{1}'.format( + ', '.join(selected_sessions), _ls_out() + ) + elif args[0] == 'none': + global selected_sessions + selected_sessions = [] + + return modes.debugging, 'No sessions selected'.format( + ', '.join(selected_sessions), _ls_out() + ) + + non_existing_sessions = [ + id for id in args if not session_manager.exists(id) + ] + if non_existing_sessions: + raise errors.BadArgsError( + 'select', 'Session(s) {0} do not exist.'. + format(', '.join(non_existing_sessions)) + ) + + global selected_sessions + selected_sessions = args + return modes.debugging, 'Selected session(s) #{0}\n{1}'.format( + ', '.join(selected_sessions), _ls_out() + ) + + +def _kill_all(): + for s_i in session_manager.list_sessions(): + if s_i.is_alive(): + s_i.query('-gdb-exit') + session_manager.kill_all() + + global session_manager + global sessions_history + session_manager = None + sessions_history = None + + +def end_sessions(args): + # Verify command syntax + if len(args) != 0: + raise errors.BadArgsError( + 'ls', 'This command does not accept arguments' + ) + + _kill_all() + return modes.offline, None + + +def quit(args): + '''Exit scimitar''' + # Verify command syntax + if len(args) != 0: + raise errors.BadArgsError( + 'ls', 'This command does not accept arguments' + ) + + _kill_all() + return modes.quit, None + + +def _ensure_sessions_selected(cmd): + '''Verifies that there are sessions selected at this moment.''' + if not selected_sessions: + raise errors.CommandFailedError( + cmd, + 'No session(s) selected. Debugging mode failed to start. (Maybe init_debugging_mode() was not called?)' + ) + +def _ensure_valid_sessions_selected(cmd): + '''Verifies if all selected sessions are still alive''' + non_existing_sessions = [ + id for id in selected_sessions if not session_manager.exists(id) + ] + if non_existing_sessions: + raise errors.BadArgsError( + cmd, 'Cannot proceed. Dead session(s): {0}.'. + format(', '.format(non_existing_sessions)) + ) + +def message_history(args): + '''Prints the selected sessions' history records at index args[0]''' + _ensure_sessions_selected('history') + + index = -1 + if len(args) == 1: + try: + index = -int(args[0]) + except ValueError: + raise errors.BadArgsError( + 'history', 'history[ ]' + ) + + if index > history_length: + raise errors.BadArgsError( + 'history', 'Selected record does not exist in the history' + ) + + results = [] + for tag in selected_sessions: + # Output header + results.append('~~~ Scimitar - Session: {} ~~~'.format(tag)) + try: + ind_rec, cout, tout, lout = sessions_history[tag][index] + if ind_rec: + # Check the type of indicator + if ind_rec[0] == mi.indicator_error: + results.append(format_error(ind_rec[1])) + elif ind_rec[0] == mi.indicator_exit: + session_manager.remove(tag) + sessions_history.remove(tag) + results.append(format_error('Session {} died.', tag)) + else: + results.append(cout) + else: + results.append(''.join([cout, tout, lout])) + except IndexError: + results.append(format_error('Scimitar: Record does not exist')) + return modes.debugging, '\n'.join(results) + + +def gdb_exec(cmd): + '''Runs the provided user command cmd on all selected sessions.''' + + class RemoteCommandExecutingThread(threading.Thread): + '''This thread type is responsible for running commands on terminal''' + + def __init__(self, term, cmd): + super(RemoteCommandExecutingThread, self).__init__() + self.term = term + self.cmd = cmd + self.error = None + self.result = None + + def run(self): + '''Start the execution''' + try: + self._run() + except Exception as e: + self.error = e + + def _run(self): + # Send the command + gdb_response = self.term.query(self.cmd) + # In case GDB dies + if gdb_response in (r'^exit', r'^kill'): + raise console.SessionDiedError + else: + self.result = mi.parse(gdb_response) + + def report(self): + if self.error: + raise self.error + return self.result + + def _sanitize_gdb_command(cmd): + '''Explicitly tell GDB that this is not an MI command following. Tries to + differentiate between control signals and commands.''' + if cmd and not repr_str_dict.has_key(cmd[0]): + return ['-interpreter-exec', 'console'] + cmd + return cmd + + def _parallel_exec_async(cmd, tag, target_sessions): + '''Start running the provided cmd per each target session in separate + thread ''' + tasks = {} + for tag in target_sessions: + # Get the session + cs = session_manager.get(tag) + exctr = RemoteCommandExecutingThread(cs, ' '.join(cmd)) + exctr.start() + tasks[tag] = exctr + return tasks + + def _collect_exec_results(tasks): + '''Wait until threads finish and then collect the results.''' + # If sessions died during execution collect them + session_casualties = [] + # Results + results = [] + + # Go through the results + for tag, task in tasks.iteritems(): + task.join() + try: + mi_response = task.report() + # Add it to message history stash + sessions_history[tag].append(mi_response) + # Output header + results.append('~~~ Scimitar - Session: {} ~~~'.format(tag)) + ind_rec, cout, tout, lout = mi_response + # Check the type of indicator + # Error + if ind_rec[0] == mi.indicator_error: + results.append(format_error(ind_rec[1])) + # Exit + elif ind_rec[0] == mi.indicator_exit: + session_manager.remove(tag) + sessions_history.remove(tag) + results.append(format_error('Session {} died.', tag)) + # Connected, Success, etc + else: + results.append(cout) + # When the session is dead + except console.SessionDiedError: + # It is not a session we can work with in future + session_manager.remove(tag) + sessions_history.remove(tag) + selected_sessions.remove(tag) + # Add this session to casualty list + session_casualties += [tag] + return results, session_casualties + + # + # gdb_exec starts here + # + + # Ensure there are selected sessions + _ensure_sessions_selected('gdb:cmd(?)') + # Make sure selected sessions are valid + _ensure_valid_sessions_selected('gdb:cmd(?)') + + # GDB launch command + cmd = _sanitize_gdb_command(cmd) + + # Threads that run the command + tasks = _parallel_exec_async(cmd, tag, selected_sessions) + + # Wait for all commands to finish + session_casualties, results = _collect_exec_results(tasks) + + # In case we had dead sessions the command has essentially failed. + if session_casualties: + results.append( + format_error( + 'Session(s) {} died.', ', '.join(session_casualties) + ) + ) + return modes.debugging, '\n'.join(results) + + +def debug(args): + import pdb + pdb.set_trace() + return modes.debugging, None + + +commands = { + 'ls': (list_sessions, None), + 'select': (select_session, select_session_complete), + 'debug': (debug, None), # HACK: For debugging only + 'history': (message_history, None), + 'end': (end_sessions, None), + 'quit': (quit, None), +} + + +def process(cmd, args): + if cmd in commands: + return commands[cmd][0](args) + else: + return gdb_exec([cmd] + (args or [])) + raise errors.CommandImplementationIncompleteError + + +class DebugSessionCommandCompleter(CommandCompleter): + + def _complete_command(self): + return commands.keys() + + def _complete_command_arguments(self, cmd, args): + if commands.has_key(cmd) and commands[cmd][1]: + return commands[cmd][1](args) + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/sessions/offline_session.py b/scimitar/sessions/offline_session.py new file mode 100644 index 0000000..2c41eb9 --- /dev/null +++ b/scimitar/sessions/offline_session.py @@ -0,0 +1,380 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +import re +import threading +import pexpect + +import modes +import errors +import console +import mi_interface +import debug_session +import schedulers.investigator as csssi # chief scimitar scheduler system investigator +from util import print_ahead, print_out, print_info, print_warning +from config import settings +from command_completer import CommandCompleter + +gdb_config = settings['gdb'] +hops = console.HopManager() +default_terminal = None +default_terminal_scheduler = None + + +def _establish_default_terminal(reestablish = False): + global default_terminal + if not default_terminal: + default_terminal = console.Terminal(hops.list_hops()) + default_terminal.connect() + elif reestablish: + global default_terminal_scheduler + default_terminal.close() + + default_terminal_scheduler = None + default_terminal = None + + +def _ensure_scheduler_exists(cmd): + if not default_terminal_scheduler: + try: + global default_terminal_scheduler + default_terminal_scheduler = csssi.detect_scheduler( + default_terminal + ) + except csssi.NoSchedulerFoundError: + if not cmd: + return False + raise errors.CommandFailedError( + cmd, + 'Unable to detect the scheduling system type on this machine.' + ) + return True + + +def job_complete(args): + if len(args) > 1: + return [] + + _establish_default_terminal() + if not _ensure_scheduler_exists(None): + return [] + + valid_choices = csssi.ls_user_jobs(default_terminal) + if not valid_choices: + return [] + + return ['auto'] + valid_choices + + +def job(args): + # Verify command syntax + if len(args) > 2: + raise errors.BadArgsError('job', 'job[ | [ ]]') + + # Basic checks + _establish_default_terminal() + _ensure_scheduler_exists('job') + + # If job name is not provided then probably only one job is active + if len(args) == 0 or args[0] == 'auto': + try: + job_id = csssi.detect_active_job(default_terminal) + # No active jobs + except csssi.NoActiveJobError: + raise errors.CommandFailedError( + 'job', 'No active user jobs found. Cannot proceed.' + ) + # More than one job is active + except csssi.MoreThanOneActiveJobError: + raise errors.CommandFailedError( + 'job', 'Found more than one job. Cannot proceed.' + ) + # Job Id provided + else: + job_id = args[0] + # In case app name is provided + app = args[1] if len(args) == 2 else None + + # Hosts and PIDs will be stored here + pid_dict = None + + # List job nodes and processes + try: + pid_dict = csssi.ls_job_pids(default_terminal, job_id, app) + # Job does not exist + except csssi.InvalidJobError: + raise errors.CommandFailedError( + 'job', '{0} does not seem to be a valid job id'.format(job_id) + ) + # Cannot determine app + except csssi.NoRunningAppFoundError: + raise errors.CommandFailedError( + 'job', + 'No application name lookup pattern provided and automatic MPI application detection did not succeed. Ensure job {0} is actually running a distributed application and provide the application name'. + format(job_id) + ) + + # Launch GDB and attach to PIDs + session_manager, msgs = _attach_pids(pid_dict) + + # Initialize the debugging session + return debug_session.init_debugging_mode(session_manager, msgs) + + +def list_jobs(args): + # Verify command syntax + if args: + raise errors.BadArgsError( + 'jobs', 'This command does not accept arguments.' + ) + + _establish_default_terminal() + _ensure_scheduler_exists('jobs') + items = csssi.ls_user_jobs(default_terminal) + jobs_str = '\t'.join(items) + return modes.offline, jobs_str + + +class _AttachPidThread(threading.Thread): + + def __init__(self, host, pid, tag, cmd): + super(_AttachPidThread, self).__init__() + self.host = host + self.pid = pid + self.tag = tag + self.cmd = cmd + self.error = None + + self.term = None + self.mi_response = None + + def run(self): + try: + self.term = console.Terminal( + hops.list_hops(), + target_host = self.host, + meta = self.pid, + tag = self.tag, + prompt_re = r'\(gdb\)\ \r\n', + exit_re = r'&"quit\n"|\^exit', + ) + self.term.connect() + + gdb_response = self.term.query(self.cmd) + try: + self.mi_response = mi_interface.parse(gdb_response) + except pexpect.ExceptionPexpect as e: + raise errors.CommandFailedError('attach', 'attach', e) + except Exception as e: + self.error = e + + def report(self): + if self.error: + raise self.error + return self.term, self.mi_response + + +def _attach_pids(pid_dict): + mgr = console.SessionManager() + + gdb_cmd = gdb_config['cmd'] + gdb_attach_tmpl = gdb_config['attach'] + + tag_counter = 0 + + tasks = {} + msgs = {} + + # Start GDB instances + for host in pid_dict.iterkeys(): + for pid in pid_dict[host]: + tag_counter += 1 + tag = str(tag_counter) + + # Build the command line and launch GDB + cmd = gdb_cmd + [gdb_attach_tmpl.format(pid = pid)] + cmd_str = ' '.join(cmd) + + attach_pid_task = _AttachPidThread(host, pid, tag, cmd_str) + tasks[tag] = attach_pid_task + attach_pid_task.start() + + for tag, task in tasks.iteritems(): + print_info( + 'Connecting to Process "{pid}" on "{host}"...', + host = task.host or 'localhost', + pid = task.pid + ) + + task.join() + print_info('Connected.') + + session, mi_response = task.report() + + mgr.add(session) + msgs[tag] = mi_response + + print_info('Beginning debugging session...') + return mgr, msgs + + +def attach(args): + + def _find_dead_pids_host(host, pids): + dead_pids = [] + + _establish_default_terminal() + _ensure_scheduler_exists('jobs') + for pid in pids: + if not default_terminal.is_pid_alive(pid): + host_path = '.'.join(hops.list_hops()) + if host: + host_path += '.' + host + dead_pids.append( + '{host}:{pid}'.format( + host = host_path or 'localhost', pid = pid + ) + ) + return dead_pids + + def _find_dead_pids(pid_dict): + # Check the status of all provided PIDs + dead_pids = [] + for host, pids in pid_dict.iteriterms(): + # Establish a connection per each process + dead_pids.extend(_find_dead_pids_host(host, pids)) + return dead_pids + + def _parse_group_pids(expr): + pid_dict = {} + for app_instance in re.finditer('((?:(\w+):)?(\d+))', expr): + host = app_instance.group(2) + pid = int(app_instance.group(3)) + + if pid_dict.has_key(host): + pid_dict[host] += [pid] + else: + pid_dict[host] = [pid] + + args_string = ' '.join(args) + # Verify command syntax + if len(args) < 1 or not re.match('(?:(?:\w+:)?\d+|\s)+', args_string): + raise errors.BadArgsError( + 'attach', 'attach [:][ [:] [...]]' + ) + + # Group by host + pid_dict = _parse_group_pids(args_string) + + # Check the status of all provided PIDs + dead_pids = _find_dead_pids(pid_dict) + + # Stop if all processes are alive + if len(dead_pids) != 0: + raise errors.CommandFailedError( + 'attach', + 'Invalid PIDs provided: {0}'.format(' ,'.join(dead_pids)) + ) + + # Launch GDB and attach to PIDs + session_manager, msgs = _attach_pids(pid_dict) + + # Initialize the debugging session + return debug_session.init_debugging_mode(session_manager, msgs) + + +def quit(args): + + def _cleanup_default_terminal(): + if default_terminal: + default_terminal.close() + + _cleanup_default_terminal() + return modes.quit, None + + +def hop(args): + # List hops + if not args: + items = hops.list_hops() + hops_str = '->'.join(items) + return modes.offline, hops_str + + for host in args: + hops.add(host) + + _establish_default_terminal(reestablish = True) + + return modes.offline, None + + +def unhop(args): + # Verify command syntax + if len(args) > 1: + raise errors.BadArgsError( + 'unhop', 'unhop[ ].' + ) + + n_hops_to_remove = 1 + if len(args) == 1: + try: + n_hops_to_remove = int(args[0]) + except ValueError: + raise errors.BadArgsError( + 'unhop', 'unhop[ ].' + ) + try: + for _ in range(n_hops_to_remove): + hops.remove_last() + except console.NoHopsError: + raise errors.CommandFailedError( + 'unhop', 'No more hops currently exist. Nothing can be removed' + ) + + _establish_default_terminal(reestablish = True) + + return modes.offline, None + + +def debug(args): + import pdb + pdb.set_trace() + + return modes.offline, None + + +commands = { + 'hop': (hop, None), + 'unhop': (unhop, None), + 'job': (job, job_complete), + 'jobs': (list_jobs, None), + 'attach': (attach, None), + 'debug': (debug, None), # HACK: For debugging only + 'quit': (quit, None), +} + + +def process(cmd, args): + if cmd in commands: + return commands[cmd][0](args) + raise errors.UnknownCommandError(cmd) + + +class OfflineSessionCommandCompleter(CommandCompleter): + + def _complete_command(self): + return commands.keys() + + def _complete_command_arguments(self, cmd, args): + if commands.has_key(cmd) and commands[cmd][1]: + return commands[cmd][1](args) + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/util/__init__.py b/scimitar/util/__init__.py similarity index 87% rename from util/__init__.py rename to scimitar/util/__init__.py index fc3b85f..66e5c54 100644 --- a/util/__init__.py +++ b/scimitar/util/__init__.py @@ -10,7 +10,7 @@ # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # from . import vt100 -from .exceptions import * -from .utils import * +from .input_helpers import * +from .print_helpers import * # vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/scimitar/util/input_helpers.py b/scimitar/util/input_helpers.py new file mode 100644 index 0000000..c4929fc --- /dev/null +++ b/scimitar/util/input_helpers.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# +# Scimitar: Ye Distributed Debugger +# +# Copyright (c) 2016-2017 Parsa Amini +# Copyright (c) 2016 Hartmut Kaiser +# Copyright (c) 2016 Thomas Heller +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +from sys import exit +from datetime import datetime as time +import signal +import re +import select +import print_helpers +import prompt_toolkit as ptk +import os.path + +from . import signals +from . import vt100 as v +from config import settings + +signals_config = settings['signals'] +FORMAT_CONSTS = {'u1': v.format._underline_on, 'u0': v.format._underline_off} +history_file = None +custom_completer_type = None + + +def raw_input_async(prompt = '', timeout = 5): + """This is a blocking user input read function. + It has to be running inside the main thread because it contains code handling signals. + Despite what the title suggests this version IS NOT ASYNC. + ALARM signal is not enabled.""" + #signal.signal(signal.SIGINT, signal_handler) # + signal.signal(signal.SIGTSTP, signals.__stop_handler) # + signal.signal(signal.SIGQUIT, signals.__quit_handler) # + #signal.signal(signal.SIGALRM, signals.__alarm_handler) + #signal.alarm(timeout) + + try: + text = ptk.prompt(prompt, patch_stdout=True, history=history_file, completer=custom_completer_type) + text_parts = text.split() + #signal.alarm(0) + return text_parts, None + #except signals.AlarmSignal: + # return None + except signals.StopSignal: # SUB (Substitute) 0x1a + return None, '\x1a' + except signals.QuitSignal: # FS (File Separator) 0x1c + # HACK: For debugging. Disable for release + import pdb + pdb.set_trace() + return None, '\x1c' + except KeyboardInterrupt: # ETX (End of Text) 0x03 + # HACK: Disable for production + if raw_input_async.last_kill_sig: + if (time.now() - raw_input_async.last_kill_sig + ).seconds < signals_config['sigkill_last']: + # The user is frantically sending s + if raw_input_async.kill_sigs >= signals_config['sigkill'] - 1: + print_helpers.print_out( + '\rGot too many {u1}{u0}s. ABAAAAAAANDON SHIP!' + ) + cleanup_terminal() + exit(0) + else: + raw_input_async.kill_sigs = 0 + else: + raw_input_async.kill_sigs = 0 + raw_input_async.kill_sigs += 1 + raw_input_async.last_kill_sig = time.now() + # HACK: NOTE: Only this line stays + return None, '\x03' + except EOFError: # EOT (End of Transmission) 0x04 + return None, '\x04' + # Possibly raised when history file is not accessible + # IOError: [Errno 13] Permission denied: u'' + except IOError as e: + raise e + finally: + signal.signal(signal.SIGTSTP, signal.SIG_DFL) + signal.signal(signal.SIGQUIT, signal.SIG_DFL) + #signal.signal(signal.SIGALRM, signal.SIG_IGN) + # We should never reach here + return None, None + + +def init_terminal(): + # Load history + global history_file + history_file_path = os.path.join(os.path.expanduser('~'), '.scimitar_history') + history_file = ptk.history.FileHistory(history_file_path) + + +def register_completer(cmpl_type): + global custom_completer_type + custom_completer_type = cmpl_type + + +def cleanup_terminal(): + # Clean up the terminal before letting go + v.unlock_keyboard() + v.format.clear_all_chars_attrs() + +# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/util/utils.py b/scimitar/util/print_helpers.py similarity index 50% rename from util/utils.py rename to scimitar/util/print_helpers.py index 71798ba..b177a78 100644 --- a/util/utils.py +++ b/scimitar/util/print_helpers.py @@ -9,16 +9,11 @@ # Distributed under the Boost Software License, Version 1.0. (See accompanying # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # -from . import config -from . import signals -from . import vt100 as v -from sys import stdout, exit -from datetime import datetime as time +from sys import stdout import signal import readline -import re -import select from itertools import chain +from . import vt100 as v FORMAT_CONSTS = {'u1': v.format._underline_on, 'u0': v.format._underline_off} @@ -28,6 +23,23 @@ def merge_dicts(dict_a, dict_b): return dict(chain(dict_a.items(), dict_b.items())) +def _format_color(expr, color, *args, **kwargs): + t = v.format._clear_all_chars_attrs + v.format._alternate_intesity_on + color + expr + v.format._clear_all_chars_attrs + return t.format(*args, **merge_dicts(kwargs, FORMAT_CONSTS)) + + +def format_error(expr, *args, **kwargs): + return _format_color(expr, v.format._fg_red, *args, **merge_dicts(kwargs, FORMAT_CONSTS)) + + +def format_warning(expr, *args, **kwargs): + return _format_color(expr, v.format._fg_yellow, *args, **merge_dicts(kwargs, FORMAT_CONSTS)) + + +def format_info(expr, *args, **kwargs): + return _format_color(expr, v.format._fg_blue, *args, **merge_dicts(kwargs, FORMAT_CONSTS)) + + def print_out(expr, ending = '\n', *args, **kwargs): '''Standard print() function in Scimitar''' stdout.write( @@ -45,6 +57,24 @@ def print_error(expr, *args, **kwargs): ) +def print_warning(expr, *args, **kwargs): + '''Standard print() function for warinings in Scimitar''' + print_out( + '\r' + v.format._clear_all_chars_attrs + + v.format._alternate_intesity_on + v.format._fg_yellow + expr + + v.format._clear_all_chars_attrs, *args, **kwargs + ) + + +def print_info(expr, *args, **kwargs): + '''Standard print() function for information messages in Scimitar''' + print_out( + '\r' + v.format._clear_all_chars_attrs + + v.format._alternate_intesity_on + v.format._fg_blue + expr + + v.format._clear_all_chars_attrs, *args, **kwargs + ) + + def print_ahead(expr, prompt = '', *args, **kwargs): '''Prints expr above the current line''' current_input = readline.get_line_buffer() @@ -99,97 +129,9 @@ def repr_str(string): return t -# NOTE: What did I write this function for? -def stream_readline(stream): - return stream.readline() - - # NOTE: I don't remember why I wrote this one either def stream_writeline(msg, stream): stream.write(msg) stream.flush() - -def attempt_read(stream, retries = -1, timeout = -1, pattern = None): - """Tries to read until a condition is met. - Returns empty if nothing was read.""" - before_read = time.now() - - is_ready = select.poll() - is_ready.register(stream, select.POLLIN) - - out_text = '' - q = 0 - while True: - if timeout > 0 and (time.now() - before_read).seconds >= timeout: - break - - if not is_ready.poll(0): - q += 1 - if retries > 0 and q >= retries: - break - else: - continue - - if pattern and re.search(pattern, out_text): - break - - out_text += stream_readline(stream) - return out_text - - -# MERGE: asyncio_processing_loop (939bad3d2718407e8b07176c14839ba0) -def raw_input_async(prompt = '', timeout = 5): - """This is a blocking user input read function. - It has to be running inside the main thread because it contains code handling signals. - Despite what the title suggests this version IS NOT ASYNC. - ALARM signal is not enabled.""" - #signal.signal(signal.SIGINT, signal_handler) # - signal.signal(signal.SIGTSTP, signals.__stop_handler) # - signal.signal(signal.SIGQUIT, signals.__quit_handler) # - #signal.signal(signal.SIGALRM, signals.__alarm_handler) - #signal.alarm(timeout) - - try: - text = raw_input(prompt) - text_parts = text.split() - #signal.alarm(0) - return text_parts, None - #except signals.AlarmSignal: - # return None - except signals.StopSignal: # SUB (Substitute) 0x1a - return None, '\x1a' - except signals.QuitSignal: # FS (File Separator) 0x1c - # HACK: For debugging. Disable for release - import pdb - pdb.set_trace() - return None, '\x1c' - except KeyboardInterrupt: # ETX (End of Text) 0x03 - # HACK: Disable for production - if raw_input_async.last_kill_sig: - if (time.now() - raw_input_async.last_kill_sig - ).seconds < config.settings['signals']['sigkill_last']: - # The user is frantically sending s - if raw_input_async.kill_sigs >= config.settings['signals'][ - 'sigkill' - ] - 1: - print_out('\rGot {u1}{u0}. ABAAAAAAANDON SHIP!') - exit(0) - else: - raw_input_async.kill_sigs = 0 - else: - raw_input_async.kill_sigs = 0 - raw_input_async.kill_sigs += 1 - raw_input_async.last_kill_sig = time.now() - # HACK: NOTE: Only this line stays - return None, '\x03' - except EOFError: # EOT (End of Transmission) 0x04 - return None, '\x04' - finally: - signal.signal(signal.SIGTSTP, signal.SIG_DFL) - signal.signal(signal.SIGQUIT, signal.SIG_DFL) - #signal.signal(signal.SIGALRM, signal.SIG_IGN) - # We should never reach here - return None, None - # vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/util/signals.py b/scimitar/util/signals.py similarity index 95% rename from util/signals.py rename to scimitar/util/signals.py index 907f340..c85a6ce 100644 --- a/util/signals.py +++ b/scimitar/util/signals.py @@ -9,7 +9,7 @@ # Distributed under the Boost Software License, Version 1.0. (See accompanying # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # -from .exceptions import * +from errors import ScimitarError class AlarmSignal(ScimitarError): diff --git a/util/vt100/__init__.py b/scimitar/util/vt100/__init__.py similarity index 100% rename from util/vt100/__init__.py rename to scimitar/util/vt100/__init__.py diff --git a/util/vt100/apply.py b/scimitar/util/vt100/apply.py similarity index 100% rename from util/vt100/apply.py rename to scimitar/util/vt100/apply.py diff --git a/util/vt100/cursor.py b/scimitar/util/vt100/cursor.py similarity index 100% rename from util/vt100/cursor.py rename to scimitar/util/vt100/cursor.py diff --git a/util/vt100/edit.py b/scimitar/util/vt100/edit.py similarity index 100% rename from util/vt100/edit.py rename to scimitar/util/vt100/edit.py diff --git a/util/vt100/format.py b/scimitar/util/vt100/format.py similarity index 100% rename from util/vt100/format.py rename to scimitar/util/vt100/format.py diff --git a/util/vt100/keyboard.py b/scimitar/util/vt100/keyboard.py similarity index 100% rename from util/vt100/keyboard.py rename to scimitar/util/vt100/keyboard.py diff --git a/util/vt100/reports.py b/scimitar/util/vt100/reports.py similarity index 100% rename from util/vt100/reports.py rename to scimitar/util/vt100/reports.py diff --git a/util/vt100/screen.py b/scimitar/util/vt100/screen.py similarity index 100% rename from util/vt100/screen.py rename to scimitar/util/vt100/screen.py diff --git a/util/vt100/terminal.py b/scimitar/util/vt100/terminal.py similarity index 100% rename from util/vt100/terminal.py rename to scimitar/util/vt100/terminal.py diff --git a/session/debugging_session.py b/session/debugging_session.py deleted file mode 100644 index e5480a1..0000000 --- a/session/debugging_session.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Scimitar: Ye Distributed Debugger -# -# Copyright (c) 2016 Parsa Amini -# Copyright (c) 2016 Hartmut Kaiser -# Copyright (c) 2016 Thomas Heller -# -# Distributed under the Boost Software License, Version 1.0. (See accompanying -# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -# -from . import modes, local_session as _local_s, remote_session as _remote_s -from .exceptions import * -from util import config, print_ahead - - -####################### -# mode: debugging -####################### -def process(pids): - raise CommandImplementationIncompleteError - - -def quit(args): - raise CommandImplementationIncompleteError - -# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/session/local_session.py b/session/local_session.py deleted file mode 100644 index d92b42e..0000000 --- a/session/local_session.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Scimitar: Ye Distributed Debugger -# -# Copyright (c) 2016 Parsa Amini -# Copyright (c) 2016 Hartmut Kaiser -# Copyright (c) 2016 Thomas Heller -# -# Distributed under the Boost Software License, Version 1.0. (See accompanying -# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -# -from .exceptions import * -from . import modes -from util import configuration, print_ahead -import pexpect as sp - - -############################# -# mode: local -############################# -def raw(args): - # Launch GDB - raise CommandImplementationIncompleteError - - -def attach(args): - raise CommandImplementationIncompleteError - - -def list(args): - raise CommandImplementationIncompleteError - - -def quit(args): - raise CommandImplementationIncompleteError - - -commands = { - 'raw': raw, - 'attach': attach, - 'list': list, - 'quit': quit, -} - - -def process(cmd, args): - raise CommandImplementationIncompleteError - #return (modes.local, None) - - -# FIXME: Disabled for now -# MERGE: local_session (8c110db273af4a81bea68ef8686f1beb) -def launch(pids): - """Not active in this version""" - global session - - session = LocalSession() - raise CommandImplementationIncompleteError - #for pid in pids: - # new_session = _local.LocalSession(pid) - #_sessions.append(new_session) - - -def quit(args): - raise CommandImplementationIncompleteError - - -class LocalSession(): - - def __init__(self): - self.terminals = None - self.active_terminal = None - self.connect_one() - - @classmethod - def attach_to_pids(cls, pids): - raise CommandImplementationIncompleteError - - @classmethod - def raw(cls): - raise CommandImplementationIncompleteError - - @classmethod - def list(cls): - raise CommandImplementationIncompleteError - - def query(self, msg): - self.active_terminal.sendline(msg) - self.active_terminal.prompt() - return self.conn.before - - def connect_one(self): - try: - gdb_cmd = configuration.settings['gdb']['cmd'] - cmd_str = ' '.join(gdb_cmd) - conn = sp.spawn(cmd_str) - conn.expect('\s{1,2}\(gdb\) \s{1,2}') - conn.setecho(False) - if self.terminals: - self.terminals.append(conn) - else: - self.terminals = [conn] - self.active_terminal = conn - except Exception as e: - raise e - - def connect_all(self): - raise CommandImplementationIncompleteError - - def __enter__(self): - raise CommandImplementationIncompleteError - - def __exit__(self): - raise CommandImplementationIncompleteError - -# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/session/offline_session.py b/session/offline_session.py deleted file mode 100644 index 8e652d7..0000000 --- a/session/offline_session.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Scimitar: Ye Distributed Debugger -# -# Copyright (c) 2016 Parsa Amini -# Copyright (c) 2016 Hartmut Kaiser -# Copyright (c) 2016 Thomas Heller -# -# Distributed under the Boost Software License, Version 1.0. (See accompanying -# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -# -from . import modes, local_session as _local_s, remote_session as _remote_s -from .exceptions import * -from util import config, print_ahead -####################### -# mode: offline -####################### -# Valid commands: -## $ local -## $ local [ ...] -## $ remote -## $ remote -## $ remote attach [ ...] -## quit - -# Are we doing whatever we're doing locally or over SSH -# helps us to choose between pxssh/pexpect -is_this_ssh = False - - -def connect(args): - pass - - -def end(args): - pass - - -def raw(args): - pass - - -def pin(args): - pass - - -def auto(args): - pass - - -def job(args): - pass - - -init_commands = { - # Operations: - # connect - # SSHs to remote node - 'connect': connect, - # disconnect - # Disconnects remote connections - 'disconnect': disconnect, - # pin : : : .... - # Attaches to nodes and PIDs provided - 'pin': pin, - # raw - # Starts a local debugging session - 'raw': raw, - # auto - # !!! Assumes the machine it's running on has scheduler commands - # !!! Assumes there's only one active job - # Tries to find the active job, gather information about the job and - # attach to the relevant PIDs on their respective nodes - 'auto': auto, - # job - # !!! Assumes the machine it's running on has scheduler commands - # !!! Assumes is an active job we can query on this machine - # Tries to collect information about job and attach attach to - # the relevant PIDs on their respective nodes - 'job': job, -} -setting_commands = { - # Settings: - # blitz - If no relevant active jobs are found return (default) - 'blitz': 'ls', - # ambush - If relevant jobs are found but are not started, or other - # criteria is not met yet keep waiting - 'ambush': 'ls', -} - - -def local(args): - try: - pids = [] - for arg in args: - pids.append(int(arg)) - _local_s.launch(pids) - return (modes.to_local, None) - except ValueError: - raise BadArgsError( - 'local', 'Was expecting PIDs, received non-integer(s): {0}'. - format(repr(args)) - ) - _local_s.launch(pids) - raise CommandImplementationIncompleteError - #return (modes.local, None) - - -def remote(args): - if len(args) != 2: - raise BadArgsError('remote', 'remote ') - #print_ahead('Launching remote session') - _remote_s.launch(name = args[0], jobid = args[1]) - return (modes.remote, None) - - -def quit(args): - return (modes.quit, None) - - -def debug(args): - import pdb - pdb.set_trace() - return (modes.offline, None) - - -commands = { - 'local': local, - 'remote': remote, - 'quit': quit, - 'debug': debug, -} - - -def process(cmd, args): - if cmd in commands: - return commands[cmd](args) - raise UnknownCommandError(cmd) - -# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/session/remote_session.py b/session/remote_session.py deleted file mode 100644 index 1b89329..0000000 --- a/session/remote_session.py +++ /dev/null @@ -1,292 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Scimitar: Ye Distributed Debugger -# -# Copyright (c) 2016 Parsa Amini -# Copyright (c) 2016 Hartmut Kaiser -# Copyright (c) 2016 Thomas Heller -# -# Distributed under the Boost Software License, Version 1.0. (See accompanying -# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -# -from .exceptions import * -from . import modes -from util import configuration, print_ahead -import pexpect.pxssh as sp -############################## -# mode: remote -############################## - -global session - -# FIXME: RemoteSession should live in a different thread. Whenever connection -# dies or is closed session mode should change instantly. Exceptions must be -# handled properly, too - - -# TODO: Move this. GDB commands should be processed inside debugging_session -# TODO: If response didn't come by after a certain timeout passes return pending -def process(cmd, args): - """Sends a query and retrieves the response""" - response = None - line = cmd + ' '.join(args) - try: - response = session.query(line) - # FIXME: Find out what exceptions will be passed and handle them properly - except Exception as e: - pass - return (modes.remote, response) - - -def launch(name, jobid): - global session - session = RemoteSession(name, jobid) - #session.disconnect_all() - - -class RemoteSession(): - - def __init__(self, name, jobid): - """Starts an SSH connection, finds all computing nodes in the batch job - and connects to each to find the PIDs belonging to the HPX application. - NOTE: Only PBS is supported at this point""" - # NOTE: We're assuming host configurations are all in utils/config.py. - # Try to get the configuration - try: - self.config = configuration.get_host_config(name) - except configuration.HostNotConfiguredError: - raise BadArgsError( - 'remote', - '{name} not found in "utils/config.py"'.format_map( - name = name - ) - ) - # Scheduler Job ID - self.jobid = jobid - self.app_name, self.nodes, self.job = None, None, {} - # Find application name, nodes, PIDs - self.examine_job() - - self.remote_terminals = None - self.active_terminal = None - self.connect_all() - - @classmethod - def try_attach_to_job(cls, name, job_id = None): - # Try getting the config - # Get an examiner - # Query for user jobs - # If no job_id given: - # If there's only one active job: - # Pass the examiner to attempt_launch - # Else: - # Parse the list and return the results - # If job_id is given: - # If job_id is among active jobs: - # Pass the examiner to attempt_launch - # Else: - # raise an error - raise CommandImplementationIncompleteError - - @classmethod - def try_list_jobs(cls, examiner): - raise CommandImplementationIncompleteError - - @classmethod - def inspect_job(cls, examiner): - raise CommandImplementationIncompleteError - - @classmethod - def try_launch(cls, name, layout): - # Try getting the config - # Connect to login node - # Try launching - raise CommandImplementationIncompleteError - - def query(self, line): - self.active_terminal.sendline(line) - self.active_terminal.prompt() - return self.active_terminal.before - - # FIXME: Changed this to assume one locality per node. - # MERGE: hpx_pids (4c2e6efda9334f50a97498ff3df4ca37) - # TODO: This function's too long. It needs to be refactored. - def examine_job(self): - '''Attempts to retrieve job information. Returns a tuple of a) - application name, b) list of nodes, and c) a dictionary of PIDs''' - try: - # SSH to the head node. - print_ahead( - 'Connecting to {host}...', host = self.config.login_node - ) - with RemoteJobExaminer(self.config) as examiner: - # Retrieve list of nodes - self.nodes = examiner.try_list_nodes(self.jobid) - print_ahead( - 'Nodes in job {u1}{jobid}{u0}: {u1}{nodes}{u0}', - jobid = self.jobid, - nodes = ' '.join(self.nodes) - ) - # Retrieve application name - self.app_name = examiner.try_find_running_app(self.nodes[0]) - print_ahead( - 'Application name: {u1}{app_name}{u0}', - app_name = self.app_name - ) - app_short_name = self.app_name.split('/')[-1] - - # Retrieve the PIDs - self.job = examiner.try_list_pids(self.nodes, app_short_name) - print_ahead('PIDs:\n{0}', repr(self.job)) - - # Broken pipe - except sp.ExceptionPxssh as e: - raise CommandFailedError('examine_job', e.expectation) - - def connect_all(self): - '''Connects to appropriate remote machines and launches a GDB session - per each PID''' - gdb_config = configuration.settings['gdb'] - gdb_cmd = gdb_config['cmd'] - - self.remote_terminals = [] - - for node, pids in self.job.items(): - for pid in pids: - try: - conn = sp.pxssh(echo = False) - conn.login( - self.config.login_node, self.config.user, - self.config.PS1 - ) - - # Build the command line and launch GDB - gdb_cmd += [gdb_config['attach'].format(pid = pid)] - cmd = ['ssh', node].extend(gdb_cmd) - cmd_str = ' '.join(cmd) - - conn.PROMPT = gdb_config['mi_prompt_pattern'] - conn.sendline(cmd_str) - - self.remote_terminals.append(conn) - - except sp.ExceptionPxssh as e: - raise e - - self.active_terminal = self.remote_terminals[0] - - def disconnect_all(remote_terms): - '''Disconnects the SSH connections of all GDB sessions''' - if self.remote_terminal: - self.active_terminal = None - for term in self.remote_terminals: - term.close() - - def __enter__(self): - return self - - def __exit__(self, _type, _value, _traceback): - self.disconnect_all() - - -class RemoteJobExaminer: - - def __init__(self, cfg): - self.cfg = cfg - self.conn = None - - def __enter__(self): - # SSH to the head node. - self.conn = sp.pxssh(echo = False) - self.conn.login( - self.cfg.login_node, self.cfg.user, original_prompt = self.cfg.PS1 - ) - return self - - def __exit__(self, _type, _value, _traceback): - self.conn.close() - - def try_list_nodes(self, jobid): - # Get list of nodes. - node_ls_cmd = self.cfg.node_ls_cmd.format(jobid = jobid) - node_ls_raw = self.query(node_ls_cmd) - # Check if the command actually succeeded - self.verify_command_success( - ''' -Cannot list nodes in job {jobid}. Listing failed with exit status code {status_code}. -Make sure Job ID {jobid} is correct and the job has started''', - ''' -Got an unexpected response from the listing command. Cannot proceed''', - jobid = jobid - ) - # Process the result and get the hostnames - # TODO: Handle potential exceptions when the text was messed up. - # TODO: Handle potential exceptions when the function messes up. - nodes = self.cfg.node_ls_fn(node_ls_raw) - if type(nodes) is not list: - raise CommandFailedError( - 'examine_job', ''' -Processing function did not return a list''' - ) - return nodes - - def try_find_running_app(self, node_0): - # TODO: Check if it actually returned a name - # Retrieves application name - app_name_cmd = self.cfg.app_name_cmd.format(host = node_0) - app_name_raw = self.query(app_name_cmd) - # Check if the command actually succeeded - self.verify_command_success( - ''' -Cannot retrieve running application's name. Please make sure the app is -running''', ''' -Got an unexpected response while trying to retrieve running application' -name. Cannot proceed''' - ) - if not app_name_raw: - raise NoRunningAppFoundError - return self.cfg.app_name_fn(app_name_raw) - - def try_list_pids(self, nodes, app_short_name): - job = {} - # Connect and collect PIDs - for node in nodes: - # TODO: Check if it actually returned PIDs - # Build the command - pid_ls_cmd = self.cfg.pid_ls_cmd.format( - host = node, appname = app_short_name - ) - # Send the command - pids_raw = self.query(pid_ls_cmd) - # Process the list - pids = self.cfg.pid_ls_fn(pids_raw) - # See if it actually is a list or not - if type(pids) is not list: - raise CommandFailedError( - 'list_pids', ''' -Processing function did not return a list''' - ) - # Add to the dictionary - job[node] = pids - return job - - def query(self, msg): - self.conn.sendline(msg) - self.conn.prompt() - return self.conn.before - - def verify_command_success(self, fail_msg, error_msg, **kwargs): - # Check if the listing command was successful. - status = self.query('echo $?') - try: - status = int(status) - kwargs['status_code'] = status - # If it had failed - if status != 0: - raise CommandFailedError( - 'examine_job', fail_msg.format(**kwargs) - ) - except ValueError: - raise CommandFailedError('examine_job', error_msg.format(**kwargs)) - -# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/session/terminals.py b/session/terminals.py deleted file mode 100644 index 823c8cc..0000000 --- a/session/terminals.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Scimitar: Ye Distributed Debugger -# -# Copyright (c) 2016 Parsa Amini -# Copyright (c) 2016 Hartmut Kaiser -# Copyright (c) 2016 Thomas Heller -# -# Distributed under the Boost Software License, Version 1.0. (See accompanying -# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -# -''' -module scimitar.session.offline_session - -This module contains code used by the main Scimitar procedure. The code in -this module is executed in offline mode i.e. when Scimitar no debuggin -session is active and while being idle. -''' - -from . import modes, local_session as _local_s, remote_session as _remote_s -from .exceptions import * -from util import config, print_ahead -import pexpect.pxssh as sp -import pexpect as lp -import pty - - -class Terminal(): - pass - - -class LocalTerminal(Terminal): - - def __init__(): - self.connection = pty.spawn('') - #self.agent = lp.spawn('bash') - pass - - def execute(self, cmd): - if '\n' in cmd: - self.connection.println( - '__cmd_func__(){\n%s\n' % cmd + - '}; echo __"cmd_start"__; __cmd_func__; echo __"cmd_end"__; unset -f __cmd_func__' - ) - else: - self.connection.println( - 'echo __"cmd_start"__; %s; echo __"cmd_end"__' % cmd - ) - - resp = '' - while not '__cmd_start__\r\n' in resp: - resp += self.connection.read() - - resp = resp[resp.find('__cmd_start__\r\n') + 15: - ] # 15 == len('__cmd_start__\r\n') - - while not '_cmd_end__' in resp: - resp += self.connection.read() - - return resp[:resp.find('__cmd_end__')] - - def sendline(): - pass - - -class RemoteTerminal(Terminal): - - def __init__(): - pass - -# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/tools/scimitar-gdb/python/scimitar/printers/future.py b/tools/scimitar-gdb/python/scimitar/printers/future.py index 0f998a9..35580f7 100644 --- a/tools/scimitar-gdb/python/scimitar/printers/future.py +++ b/tools/scimitar-gdb/python/scimitar/printers/future.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # Scimitar: Ye Distributed Debugger -# +# # Copyright (c) 2016 Parsa Amini # Copyright (c) 2016 Hartmut Kaiser # Copyright (c) 2016 Thomas Heller @@ -21,7 +21,11 @@ def __init__(self, val, type_): # Values self.px = self.val['shared_state_']['px'] self.state_ = self.px['state_'] - self.storage_ = self.px['storage_'] + if 'storage_' in self.px: + self.storage_ = self.px['storage_'] + else: + self.storage_ = None + # Template type self.tmpl = str(self.val.type.template_argument(0)) # Conditions @@ -36,8 +40,9 @@ def __init__(self, val, type_): if self.is_void: if self.is_exception: - value_t = gdb.lookup_type('boost::exception_ptr').pointer() - self.value = self.storage_.address.cast(value_t).dereference() + #value_t = gdb.lookup_type('boost::exception_ptr').pointer() + #self.value = self.storage_.address.cast(value_t).dereference() + pass else: if self.is_value: value_t = gdb.lookup_type(self.tmpl).pointer() @@ -58,8 +63,9 @@ def to_string(self): def children(self): result = [] if self.is_void: - if self.is_exception: - result.extend([('value', self.value), ]) + pass + #if self.is_exception: + # result.extend([('value', self.value), ]) else: if self.is_value: result.extend([('value', self.value), ]) diff --git a/tools/scimitar-gdb/python/scimitar/threads/commands.py b/tools/scimitar-gdb/python/scimitar/threads/commands.py index daf96f9..837ed4b 100644 --- a/tools/scimitar-gdb/python/scimitar/threads/commands.py +++ b/tools/scimitar-gdb/python/scimitar/threads/commands.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # Scimitar: Ye Distributed Debugger -# +# # Copyright (c) 2016 Parsa Amini # Copyright (c) 2016 Hartmut Kaiser # Copyright (c) 2016 Thomas Heller @@ -148,11 +148,15 @@ def invoke(self, arg, from_tty): state.restore() return - if argv[0].beginswith('0x'): + if argv[0][0:2] == '0x':#.beginswith('0x'): thread_id = gdb.Value(int(argv[0], 16)) else: thread_id = gdb.Value(int(argv[0])) + print('%s %x' % (argv[0][0:2], thread_id)) + + thread_id = thread_id.reinterpret_cast(gdb.lookup_type('hpx::threads::thread_data').pointer()) + thread = HPXThread(thread_id) print('Switched to HPX Thread 0x%x' % thread_id) diff --git a/tools/scimitar-gdb/python/scimitar/threads/hpx_thread.py b/tools/scimitar-gdb/python/scimitar/threads/hpx_thread.py index a9009a3..1b9dcd1 100644 --- a/tools/scimitar-gdb/python/scimitar/threads/hpx_thread.py +++ b/tools/scimitar-gdb/python/scimitar/threads/hpx_thread.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # Scimitar: Ye Distributed Debugger -# +# # Copyright (c) 2016 Parsa Amini # Copyright (c) 2016 Hartmut Kaiser # Copyright (c) 2016 Thomas Heller @@ -16,43 +16,44 @@ class HPXThread(): class Context(object): + def get_reg(self, reg): + test = gdb.parse_and_eval('$' + reg) + return test; - def __init__(self): - - self.pc = get_reg('pc') - self.r15 = get_reg('r15') - self.r14 = get_reg('r14') - self.r13 = get_reg('r13') - self.r12 = get_reg('r12') - self.rdx = get_reg('rdx') - self.rax = get_reg('rax') - self.rbx = get_reg('rbx') - self.rbp = get_reg('rbp') - self.sp = get_reg('sp') - - def get_reg(reg): - return gdb.parse_and_eval('$' + reg) - - def set_reg(reg, value): + def set_reg(self, reg, value): gdb.execute( 'set ${reg} = 0x{val:x}'.format( - reg = reg, val = value & 2 ** 64 - 1 + reg = reg, val = (int("%d" % value) & (2 ** 64 - 1)) ) ) + def __init__(self): + + self.pc = self.get_reg('pc') + self.r15 = self.get_reg('r15') + self.r14 = self.get_reg('r14') + self.r13 = self.get_reg('r13') + self.r12 = self.get_reg('r12') + self.rdx = self.get_reg('rdx') + self.rax = self.get_reg('rax') + self.rbx = self.get_reg('rbx') + self.rbp = self.get_reg('rbp') + self.sp = self.get_reg('sp') + def switch(self): - prev_ctx = self - - set_reg('pc', self.pc) - set_reg('r15', self.r15) - set_reg('r14', self.r14) - set_reg('r13', self.r13) - set_reg('r12', self.r12) - set_reg('rdx', self.rdx) - set_reg('rax', self.rax) - set_reg('rbx', self.rbx) - set_reg('rbp', self.rbp) - set_reg('sp', self.sp) + #prev_ctx = self + prev_ctx = HPXThread.Context() + + self.set_reg('pc', self.pc) + self.set_reg('r15', self.r15) + self.set_reg('r14', self.r14) + self.set_reg('r13', self.r13) + self.set_reg('r12', self.r12) + self.set_reg('rdx', self.rdx) + self.set_reg('rax', self.rax) + self.set_reg('rbx', self.rbx) + self.set_reg('rbp', self.rbp) + self.set_reg('sp', self.sp) return prev_ctx @@ -66,31 +67,31 @@ def __init__(self, thread_data): # hpx::threads::thread_state_ex_enum # >, void> # > = { m_storage = 360569445166350338 }, - # }, - # component_id_ = 8198320, - # description_ = thread_description {{ [desc] {0x7ffff4aa0cb5 'call_startup_functions_action'} }}, - # lco_description_ = thread_description {{ [desc] {0x7ffff4918a9d ''} }}, - # parent_locality_id_ = 0, - # parent_thread_id_ = 0x7f3090, - # parent_thread_phase_ = 1, - # marked_state_ = hpx::threads::unknown, - # priority_ = hpx::threads::thread_priority_normal, - # requested_interrupt_ = false, - # enabled_interrupt_ = true, - # ran_exit_funcs_ = false, - # exit_funcs_ = std::deque with 0 elements, - # scheduler_base_ = 0x7cf5b8, + # }, + # component_id_ = 8198320, + # description_ = thread_description {{ [desc] {0x7ffff4aa0cb5 'call_startup_functions_action'} }}, + # lco_description_ = thread_description {{ [desc] {0x7ffff4918a9d ''} }}, + # parent_locality_id_ = 0, + # parent_thread_id_ = 0x7f3090, + # parent_thread_phase_ = 1, + # marked_state_ = hpx::threads::unknown, + # priority_ = hpx::threads::thread_priority_normal, + # requested_interrupt_ = false, + # enabled_interrupt_ = true, + # ran_exit_funcs_ = false, + # exit_funcs_ = std::deque with 0 elements, + # scheduler_base_ = 0x7cf5b8, # count_ = { # value_ = { # > = { # m_storage = 1 # }, # } - # }, - # stacksize_ = 131072, + # }, + # stacksize_ = 131072, # coroutine_ = { # m_pimpl = (boost::intrusive_ptr) 0x7fffee728180 - # }, + # }, # pool_ = 0x7e9a60 #} @@ -104,23 +105,21 @@ def __init__(self, thread_data): self.description = self.thread_data['description_'] self.lco_description = self.thread_data['lco_description_'] - combined_state = self.thread_data['current_state_']['m_storage'] + self.size_t = gdb.lookup_type('std::size_t') + combined_state = self.thread_data['current_state_']#['m_storage'] + combined_state_type = combined_state.type.template_argument(0) + state_enum_type = combined_state_type.template_argument(0) + state_ex_enum_type = combined_state_type.template_argument(1) + combined_state = int('%d' % combined_state['m_storage'].cast(self.size_t)) - current_state_type = gdb.lookup_type('hpx::threads::thread_state_enum') - self.state = combined_state >> 56 & 0xff - self.state = self.state.cast(current_state_type) - current_state_ex_type = gdb.lookup_type( - 'hpx::threads::thread_state_ex_enum' - ) - self.state_ex = combined_state >> 48 & 0xff - self.state_ex = self.state_ex.cast(current_state_ex_type) + self.state = gdb.Value((combined_state >> 56) & 0xff).cast(state_enum_type) + self.state_ex = gdb.Value((combined_state >> 48) & 0xff).cast(state_ex_enum_type) - self.size_t = gdb.lookup_type('std::size_t') stack = self.m_sp.reinterpret_cast(self.size_t) self.context = HPXThread.Context() - self.context.pc = self.deref_stack(stack + (8 * 8)) + self.context.pc = self.deref_stack(stack + (64)) self.context.r15 = self.deref_stack(stack + (8 * 0)) self.context.r14 = self.deref_stack(stack + (8 * 1)) self.context.r13 = self.deref_stack(stack + (8 * 2)) @@ -134,7 +133,7 @@ def __init__(self, thread_data): prev_context = self.context.switch() frame = gdb.newest_frame() function_name = frame.name() - p = re.compile('^hpx::util::coroutines.*$') + p = re.compile('^hpx::threads::coroutines.*$') try: while p.match(function_name): @@ -167,7 +166,7 @@ def deref_stack(self, addr): return addr.reinterpret_cast(self.size_t.pointer()).dereference() def info(self): - gdb.write(' Thread 0x{addr:x}\n'.format(addr = self.id)) + gdb.write(' Thread {addr}\n'.format(addr = self.id)) if self.m_sp.reinterpret_cast(self.m_sp.dereference().type ) > self.stack_end: gdb.write(' This thread has a stack overflow\n') diff --git a/util/config.py b/util/config.py deleted file mode 100644 index 94669ef..0000000 --- a/util/config.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Scimitar: Ye Distributed Debugger -# -# Copyright (c) 2016 Parsa Amini -# Copyright (c) 2016 Hartmut Kaiser -# Copyright (c) 2016 Thomas Heller -# -# Distributed under the Boost Software License, Version 1.0. (See accompanying -# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -# -import re -import getpass - -settings = { - 'ui': - { - 'prompt': '$ ', - }, - 'signals': - { - 'sigkill' : 5, - 'sigkill_last': 1, - # TODO: See if we need to handle the other signals. These maybe: - ## EOF - ## SIGALRM - ## SIGINT - ## SIGQUIT - ## SIGTERM - ## SIGSTOP - }, - 'gdb': - { - # GDB command line - # Supress banner, interactive mode - 'cmd' : - [ - 'gdb', - '-interpreter=mi2', # Use GDB/MI2 interface - '-quiet', # Suppress banner - '--nx', # Don't load any .gdbinits whatsoever - ], - 'attach': '--pid={pid}', - 'mi_prompt_pattern': '\[(\]gdb\[)\] \[\r\n\]+', - }, -} - -remotes = { - 'smic': - { - # MERGE: dotsshconfig (a6206aa120844233b986cb470013cf54) - #'use_dotsshconfig': True, - # NOTE: I have smic in my .ssh/config. Won't work elsewhere as is - 'login_node' : 'smic', - # FIXME: Find a way usernames can be provided with more ease - 'user' : getpass.getuser(), - 'PS1' : '[\$\#] ', - # Solution 1: Works on the head node only - ## uniq /var/spool/torque/aux/{jobid}* - # Solution 2: - ## $ checkjob {jobid} - # Sample output: - ## $ checkjob 164018 - ## job 164018 - ## - ## AName: STDIN - ## State: Running - ## Creds: user:parsa group:Users account:hpc_hpx_grav class:hybrid qos:userres - ## WallTime: 00:00:00 of 1:00:00 - ## SubmitTime: Mon Aug 22 21:38:29 - ## (Time Queued Total: 00:00:20 Eligible: 00:00:20) - ## - ## StartTime: Mon Aug 22 21:38:49 - ## TemplateSets: DEFAULT - ## NodeMatchPolicy: EXACTNODE - ## Total Requested Tasks: 60 - ## - ## Req[0] TaskCount: 60 Partition: base - ## - ## Allocated Nodes: - ## [smic361:20][smic362:20][smic363:20] - ## - ## - ## SystemID: sched - ## SystemJID: 164018 - ## - ## IWD: /home/parsa - ## StartCount: 1 - ## Partition List: base - ## Flags: INTERACTIVE - ## Attr: INTERACTIVE,checkpoint - ## StartPriority: 140075 - ## Reservation '164018' (-00:00:20 -> 00:59:40 Duration: 1:00:00) - ## - #'node_list' : "checkjob {jobid} | grep -o '\w\+:[0-9]\+\]' | sed 's/:[0-9]*\]//'", - 'node_ls_cmd' : "checkjob {jobid}", - 'node_ls_fn' : lambda x: re.findall('\[(\w+):\d+\]', x), - 'app_name_cmd' : '''ssh {host} "ps -o pid:1,cmd:1 -e" | grep -o "MPISPAWN_ARGV_[0-9]='.\+'"''', - 'app_name_fn' : lambda x: re.findall('MPISPAWN_ARGV_0=([\S]+)', x)[0].replace('"','').replace("'",''), - #'pid_ls' : 'ps -o pid:1,cmd:1 -e | grep {appname}', - 'pid_ls_cmd' : 'ssh {host} "pgrep {appname}"', - 'pid_ls_fn' : lambda x: [int(y) for y in x.split()], - }, - 'rostam': - { - # MERGE: dotsshconfig (a6206aa120844233b986cb470013cf54) - #'use_dotsshconfig': True, - # NOTE: I have rostam in my .ssh/config. Won't work elsewhere as is - 'login_node' : 'rostam', - # HACK: I hardcoded my username. Not good - 'user' : 'pamini', - 'PS1' : '[\$\#] ', - # FIXME: add rostam's config once SLURM starts working again - 'node_ls_cmd' : None, - 'node_ls_fn' : None, - 'app_name_cmd' : None, - 'app_name_fn' : None, - 'pid_ls_cmd' : None, - 'pid_ls_fn' : None, - }, - # MERGE: stampede_config (3c21aec9daba4bc49fd2d0d98ec0e46b) - # MERGE: edison_config (613a076ab3254014b55f645a7d85e529) - # MERGE: cori_config (d8459d9a002047239fb21c3c92050980) - # MERGE: bigdat_config (406ec14fae894e66ad147245ede1abda) - # MERGE: supermike2_config (08e71a6fd99246c7ad01e996dd79fea2) -} - -# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: diff --git a/util/configuration.py b/util/configuration.py deleted file mode 100644 index f2876e6..0000000 --- a/util/configuration.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Scimitar: Ye Distributed Debugger -# -# Copyright (c) 2016 Parsa Amini -# Copyright (c) 2016 Hartmut Kaiser -# Copyright (c) 2016 Thomas Heller -# -# Distributed under the Boost Software License, Version 1.0. (See accompanying -# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -# -from collections import namedtuple -from .config import * - - -class HostNotConfiguredError(Exception): - '''Raised when the target system doesn't exist in the configuration''' - pass - -# FIXME: Fields should not be hardcoded -# NOTE: Does not support default values as is although it is possible to have -# them with the following: -# HostConfig.__new__.__defaults__ = (None,) * len(HostConfig._fields) -HostConfig = namedtuple( - 'HostConfig', - 'login_node user PS1 node_ls_cmd node_ls_fn app_name_cmd app_name_fn pid_ls_cmd pid_ls_fn' -) - - -def get_host_config(name): - '''Verifies if the target system exists in the configuration and returns - the configuration dictionary as an object''' - # Check if configuration is available under name - if not name in remotes: - raise HostNotConfiguredError - # Get configuration - return HostConfig(**remotes[name]) - -# vim: :ai:sw=4:ts=4:sts=4:et:ft=python:fo=corqj2:sm:tw=79: