├── .gitignore ├── CHANGELOG.rst ├── CMakeLists.txt ├── LICENSE ├── README.md ├── config └── rosconsole │ └── debug.config ├── doc ├── Doxyfile ├── DoxygenLayout.xml ├── content │ ├── comms.md │ ├── config_file.md │ ├── design.md │ ├── main.md │ └── tutorial.md ├── doxypypy.py ├── footer.html └── images │ ├── procman-sheriff-screenshot.png │ ├── procman_icon.png │ ├── sheriff-gui-add-command-menu.png │ ├── sheriff-gui-add-command.png │ ├── sheriff-gui-command-running.png │ ├── sheriff-gui-empty.png │ ├── sheriff-gui-start-command-menu.png │ └── sheriff-gui-with-xterm-stopped.png ├── include ├── procman │ ├── exec_string_utils.hpp │ ├── procinfo.hpp │ └── procman.hpp └── procman_ros │ ├── procman_deputy.hpp │ └── socket_monitor.hpp ├── msg ├── ProcmanCmd.msg ├── ProcmanCmdDesired.msg ├── ProcmanCmdStatus.msg ├── ProcmanDeputyInfo.msg ├── ProcmanDiscovery.msg ├── ProcmanOrders.msg └── ProcmanOutput.msg ├── package.xml ├── python ├── procman-ros-sheriff.glade └── src │ └── procman_ros │ ├── __init__.py │ ├── sheriff.py │ ├── sheriff_cli.py │ ├── sheriff_config.py │ ├── sheriff_gtk │ ├── __init__.py │ ├── command_console.py │ ├── command_model.py │ ├── command_treeview.py │ ├── deputies_treeview.py │ ├── sheriff_dialogs.py │ └── sheriff_gtk.py │ └── sheriff_script.py ├── scripts └── sheriff ├── setup.py ├── src ├── procman │ ├── exec_string_utils.cpp │ ├── procinfo_generic.cpp │ ├── procinfo_linux.cpp │ └── procman.cpp └── procman_ros │ ├── procman_deputy.cpp │ └── socket_monitor.cpp └── test └── test.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | 4 | # build directories 5 | build 6 | pod-build 7 | 8 | # doxygen output 9 | /doc/html -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package procman_ros 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 6 | 7 | 0.1.7 (2022-10-27) 8 | ------------------ 9 | * fix conversion from ms to s 10 | * fix not exiting correctly due to unassigned variable when not starting roscore 11 | * roscore is correctly terminated on window close, fixes `#30 `_ 12 | * Contributors: David Wisth, Michal Staniaszek 13 | 14 | 0.1.6 (2022-05-20) 15 | ------------------ 16 | * add wait until script is finished 17 | * Contributors: David Wisth 18 | 19 | 0.1.5 (2022-02-22) 20 | ------------------ 21 | * Fix console output not updating correctly 22 | * Contributors: Michal Staniaszek 23 | 24 | 0.1.4 (2022-02-07) 25 | ------------------ 26 | * Revert "orders/info subscribers no longer use infinite queue size", this caused deputies to often lose communication for longer periods of time 27 | * Contributors: Michal Staniaszek 28 | 29 | 0.1.3 (2022-02-03) 30 | ------------------ 31 | * belatedly set version to 0.1.2 and add changelog up to that point based on semi-arbitrary breakpoints 32 | * orders/info subscribers no longer use infinite queue size 33 | I think this could cause issues with the deputy updates and maybe with the stale 34 | orders issue. After disconnecting from the roscore wifi, deputy treeview shows 35 | red on the deputies as expected. Previously, reconnecting seemed to take a long 36 | time. With this I think the info messages come through quicker 37 | * when using observer mode no longer need to specify a config file, fixes `#33 `_ 38 | * make sheriff node anonymous so that observers can be run, attempts to fix `#32 `_ 39 | * ignore config when starting in observer mode, rather than exiting 40 | * Contributors: Michal Staniaszek 41 | 42 | 0.1.2 (2021-05-21) 43 | ------------------ 44 | * build with C++14, Correct usage of ros timestamp tonsec to match with microsecond timestamp_now, use digit separators, timestamp_now uses chrono 45 | This should fix the problem of getting complains in the sheriff of stale orders messages, this was caused by the ros timestamp returning nsecs but not correctly converting it to microsecs in the stale orders check 46 | Digit separators for better readability, require c++14 47 | * make the procman icon available when the package is installed, to make debian function too 48 | * add very simple icon 49 | * add note about environment variables 50 | * update readme to be clearer about names of deputies and what lone-ranger does 51 | * fix configs having to be loaded twice before anything was actually loaded 52 | fixes `#21 `_ 53 | This issue was caused by an extraneous else in the load config which would skip actually loading the requested config if there were any commands already loaded. Just removing the else is not enough because one of the preconditions later down the line is that there are no commands loaded. Because the remove_command function is only a scheduled removal, I added a short wait in after sending those commands. This wait may not be reliable if the network is slow, but any alternative solutions like checking if commands are scheduled for removal rather than just checking if they have already been removed is more difficult. 54 | * remove some final remaining bits of lcm stuff in the code. Apparently never encountered in standard operation so should be fine... 55 | * fix scripts not being able to call other scripts 56 | This issue is caused by the get_script function trying to acquire a lock which is already acquired when the get_next_action function of ScriptExecutionContext tries to call get_script. This appears to be a new issue on focal/python3, but from what I can see there was no change to threading.Lock. 57 | It is be possible to fix this by using threading.RLock instead but that may break other things. 58 | * install the glade file to ensure debian package has it 59 | * add install in cmakelists for debian packaging 60 | * Update license references, add myself as maintainer update package xml 61 | * Contributors: Michal Staniaszek 62 | 63 | 0.1.1 (2021-03-09) 64 | ------------------ 65 | * change topic names to be more obviously connected to procman 66 | * update readme to reflect use of pygobject and gtk+3 67 | * fix deputy view right click popup and mnemonic 68 | * copying text from console with ctrl-c no longer crashes 69 | * script editing corrected by fixing unescaped brace and missing parameter 70 | * new script dialog correctly opens 71 | * add/edit command dialog initialisation fixed 72 | * update preferences dialog to use proper initialisation of objects 73 | * fix unescaped braces in format strings when saving configs 74 | * initialise menuitems for right click menu on command with new_with_mnemonic, fixes the underscore displaying in the menu text 75 | Seems that the mnemonics given are actually overridden by the ones provided in the glade file? 76 | * correctly initialise tag, properly displays colours in console 77 | * fix 2button_press reference and popup arguments, can start/stop things with right click 78 | * remove extra calls to reload config on command deletion when loading a config 79 | This caused an assertion error for each command which was in the previous configuration as the initial load config deleted the attribute which was being checked 80 | Appears not to have any negative effect on removal of commands 81 | * remove last remaining uses of deprecated get_data 82 | * fix functions passed to TreeViewColumn.set_cell_data_func to take new *data positional arg 83 | * fix background/text colour parsing in console 84 | * fix missing attribute crashing script loading, with open and correct binary read/write to pickle 85 | * use correct functions to access adjustment, remove some deprecated get_data calls 86 | * minimal non-crashing startup, but still has lots of errors 87 | updated a few things to use gtk3+ syntax/methods 88 | * change file() function to open(), as required in python3 89 | * remove some lcm references 90 | * Apply pygi-convert on python files 91 | https://gitlab.gnome.org/GNOME/pygobject/raw/master/tools/pygi-convert.sh 92 | Required because pygtk is no longer supported on focal 93 | https://askubuntu.com/questions/97023/why-cant-i-import-pygtk-with-python-3-2-from-pydev 94 | * do not start own roscore by default 95 | * better handling of config file errors, properly exits program 96 | * more informative add command error messages, no longer crash when there is one 97 | * fix `#16 `_ bad indent causing gui not to exit on interrupt 98 | * Increase queue sizes to prevent messages being dropped 99 | Small queues may cause command status to be unknown for arbitrarily long periods of time depending on luck of when messages are receives on pm_orders topic 100 | fixes `#14 `_ 101 | * add some super basic debug info/config 102 | * cpu load display on deputy set to 4 decimal places 103 | * warn and anonymise node when deputy name is not a valid ros name 104 | * fix unused result on system call 105 | * only ros::init after receiving deputy id 106 | Use the deputy id in the ros node name to ensure that multiple deputies don't kick each other off 107 | * change license to BSD 3 clause 108 | * Contributors: Albert Huang, Michal Staniaszek 109 | 110 | 0.1.0 (2020-07-21) 111 | ------------------ 112 | 113 | * roscore no longer persists by default after sheriff/deputy exit 114 | * Merge pull request `#12 `_ from ori-drs/fix-mem-cpu-usage 115 | Fix incorrect display of memory/cpu usage for commands which spawn children 116 | * add function to aggregate memory and cpu for parent+child processes and use it instead of only looking at the parent 117 | Also format procinfo_linux 118 | * better variable names, no longer use array to store process/system info 119 | * wait until the core is available in parent before continuing 120 | * deputy can now start a roscore if one does not exist, python roscore start variable named to be less confusing 121 | * make observer and lone ranger mutually exclusive 122 | * sheriff now starts roscore if one does not exist yet 123 | * use host instead of deputy as the key for deputy names, to keep compatibility with existing config files 124 | * use idle add in procman output callback, this should fix segfaults as described in `#3 `_ 125 | * stop using ros timers, they may be causing threading issues 126 | * remove timers from event loop but retain socket monitoring 127 | * Merge branch 'master' into remove-eventloop 128 | * deputy timers now ros walltimers, try moving some stuff out of eventloop 129 | * update readme with rosrun syntax 130 | * partial solution for `#4 `_, but still using time functions from both ros and system 131 | * fix `#7 `_, event loop quit now calls ros shutdown, remove duplicate headers 132 | * move deputy time initialisation into constructor body to avoid issues when deputy starts before roscore 133 | * Fixes `#5 `_ where starting deputy before roscore can cause a segfault 134 | * shorten procman_ros_sheriff and deputy to just sheriff and deputy 135 | * fix script output not appearing in text box 136 | * add publishers and subscribers, fix run function to process ros messages 137 | * procman orders message is correctly sent 138 | * deputy publishes info about itself and sheriff receives it 139 | * make unused lambda args explicit, use ros timers instead of gobject in some places 140 | * argparse in sheriff_cli 141 | * manual conversion of % formatting to .format 142 | * apply black formatting 143 | * apply pyupgrade to change formatting strings and other older python stuff 144 | * fix indexing into argparse namespace 145 | * apply 2to3 script to update print and other statements 146 | * use argparse instead of getopt 147 | * non-crashing system which can be run with rosrun and no need for install command 148 | * cmakelists installs some more files into the correct place, renamed package to procman_ros 149 | Removed some lcm objects in the sheriff and replace a few subscribers with ros ones 150 | * Python setup, import ros message names 151 | Add some of the required files for ros python setup, not entirely complete, still need to install the script to usr/local/bin or elsewhere to make it accessible 152 | ROS message names are imported and the lcm messages no longer are, and replaced references to lcm messages, but didn't change anything in terms of processing so everything still doesn't work 153 | * Contributors: Michal Staniaszek 154 | 155 | 0.0.1 (2020-05-04) 156 | ------------------ 157 | * minimal compiling version of all c++ 158 | LCM stuff that hasn't been ported yet is commented with a //TODO 159 | * initial porting from LCM. Procman library and message generation compile 160 | * updated readme, gitignore 161 | * c++11 162 | * c++11 163 | * don't restart commands when loading from config 164 | * add LICENSE file 165 | * bugfix 166 | * fix sheriff spinning on CPU in observer mode 167 | * bugfix - socket handling 168 | * Adding easy text box copying via copy-paste. 169 | * procman-sheriff script don't set PYTHONPATH 170 | * worder thread send order bugfix 171 | * env var parsing bugfix 172 | * deputy stopcommand bugfix 173 | * fix parallel build error in lcmtypes.cmake 174 | * split deputy into libprocman and deputy 175 | * cleanups, bugfixes 176 | * add doxypypy.py 177 | * more refactoring 178 | * some refactoring 179 | * rename some Python API methods 180 | * remove SheriffCommandSpec 181 | * bugfixes 182 | * add initializer arguments to SheriffCommandSpec 183 | * process stdout/stderr nagling 184 | * bugfixes 185 | * Linux bugfixes 186 | also: 187 | - sheriff display memory RSS instead of VSIZE 188 | * deputy switch to custom event loop 189 | * stop using g_shell_parse_argv() 190 | * minor refactoring create exec_string_utils 191 | * load config remove all commands first 192 | * remove move_cmd_to_deputy 193 | * nickname -> command_id 194 | * Guard SheriffDeputyCommand, SheriffDeputy w/lock. 195 | * protect SheriffDeputy attributes with lock 196 | * lcmtypes_build_c minor cleanup 197 | * cmake pass build include path to lcmgen function 198 | * purge options from message types 199 | * deputy name/host -> deputy_id 200 | * cleanup. purge signal_slot.py 201 | * Sheriff switch from signals to SheriffListener 202 | * purge sheriff_id, use command_id as unique id. 203 | * refactor. move scripting into sheriff_script.py 204 | * procman sheriff start switch to multithreading 205 | * cleanup 206 | * src/deputy -> deputy 207 | * cleanups 208 | * bugfixes 209 | * more cleanup 210 | * some cleanups 211 | * procman_deputy switch to Qt5, stop using glib 212 | * VariableExpander 213 | * more c++ conversions 214 | * Procman struct -> class 215 | * more c++ conversions 216 | * remove DeputyCommand::sheriff_id 217 | * procman_cmd_t -> ProcmanCommand 218 | * more c++ conversions 219 | * remove procman_cmd_t::user 220 | * c struct -> C++ struct 221 | * GList -> std::vector 222 | * start using std::map instead of GHashTable 223 | * convert some glib types to stl 224 | * procinfo split to procinfo\_{generic,linux} 225 | also: 226 | - start replace GArray with std::vector 227 | - rename procman_cmd_t::cmd_id -> sheriff_id 228 | - rename procman_cmd_t::cmd_name -> cmd_id 229 | * deputy add namespace procman 230 | * procman deputy begin conversion to c++ 231 | * rename lcm types 232 | * move lcmtypes into package procman_lcm 233 | * cleanup build system 234 | * remove bot\_ prefix 235 | * remove legacy messages 236 | * import bot2-procman 237 | * Contributors: Albert Huang, Benjamin Brown, Marco Camurri, Pedro Vaz Teixeira 238 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0.2) 2 | project(procman_ros) 3 | 4 | add_compile_options(-std=c++14) 5 | 6 | find_package(catkin REQUIRED COMPONENTS 7 | message_generation 8 | roscpp 9 | rospy 10 | std_msgs 11 | ) 12 | 13 | 14 | ## Uncomment this if the package has a setup.py. This macro ensures 15 | ## modules and global scripts declared therein get installed 16 | ## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html 17 | catkin_python_setup() 18 | 19 | ## Generate messages in the 'msg' folder 20 | add_message_files(FILES 21 | ProcmanCmdDesired.msg 22 | ProcmanCmd.msg 23 | ProcmanCmdStatus.msg 24 | ProcmanDeputyInfo.msg 25 | ProcmanDiscovery.msg 26 | ProcmanOrders.msg 27 | ProcmanOutput.msg 28 | ) 29 | 30 | ## Generate added messages and services with any dependencies listed here 31 | generate_messages() 32 | 33 | catkin_package(INCLUDE_DIRS include 34 | LIBRARIES procman_ros 35 | CATKIN_DEPENDS message_runtime roscpp rospy) 36 | 37 | include_directories(include 38 | ${catkin_INCLUDE_DIRS}) 39 | 40 | if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") 41 | set(procinfo_cpp procinfo_linux.cpp) 42 | else() 43 | set(procinfo_cpp procinfo_generic.cpp) 44 | endif() 45 | 46 | add_library(procman_ros SHARED src/procman/exec_string_utils.cpp 47 | src/procman/${procinfo_cpp} 48 | src/procman/procman.cpp) 49 | target_link_libraries(procman_ros util) 50 | 51 | add_executable(deputy 52 | src/procman_ros/socket_monitor.cpp 53 | src/procman_ros/procman_deputy.cpp) 54 | 55 | target_link_libraries(deputy procman_ros ${catkin_LIBRARIES}) 56 | 57 | add_dependencies(deputy ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) 58 | 59 | ## Mark cpp header files for installation 60 | install(DIRECTORY include/${PROJECT_NAME}/ 61 | DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} 62 | FILES_MATCHING PATTERN "*.hpp") 63 | 64 | install(TARGETS deputy procman_ros 65 | RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} 66 | LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}) 67 | 68 | install(FILES python/procman-ros-sheriff.glade 69 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/python) 70 | 71 | install(FILES doc/images/procman_icon.png 72 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/doc/images) 73 | 74 | catkin_install_python(PROGRAMS scripts/sheriff 75 | DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}) 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Albert Huang 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Procman ROS 2 | Porting of the orignal [Procman](https://github.com/ashuang/procman) package to ROS. 3 | 4 | # Procman 5 | 6 | Procman is a tool for managing many processes distributed over one or more 7 | computers. There are several ways to use procman: 8 | 9 | ## Sheriff / Deputy GUI mode 10 | 11 | In this mode, every workstation runs a *deputy* process: 12 | 13 | ``` 14 | rosrun procman_ros deputy 15 | ``` 16 | 17 | If you like, you can specify a name for the deputy with `-i name`. By default, the deputy will use the hostname of the machine it is run on. 18 | 19 | One workstation runs a *sheriff* process, which provides a GUI to command and 20 | communicate with the deputies: 21 | 22 | ``` 23 | rosrun procman_ros sheriff procman_config.pmd 24 | ``` 25 | 26 | Using the GUI, you can: 27 | - create/edit/remove processes 28 | - start/stop/restart processes 29 | - aggregate processes together into logical groups (e.g., "Planning") 30 | - view the console output of each process 31 | - save and load process configuration files 32 | - view process statistics (memory, CPU usage) 33 | 34 | If you want to start processes on the machine running the sheriff process, the sheriff can start its own deputy. To operate in this *lone ranger* 35 | mode, run 36 | 37 | ``` 38 | rosrun procman_ros sheriff --lone-ranger 39 | ``` 40 | 41 | Alternatively, you can start a deputy independently as above. Note that when the sheriff starts a deputy its name will be `localhost`. 42 | 43 | ### Environment variables 44 | 45 | If the commands you are using a deputy to start require specific environment variables to be available, you must have them defined in the terminal when starting the deputy. 46 | 47 | ## C++ API 48 | 49 | Procman also provides a C++ API for spawning and managing child processes, 50 | comparable to the Python subprocess module. 51 | 52 | ## Build Instructions 53 | The package has been catkinized. To compile it, just run: 54 | ``` 55 | catkin build procman_ros 56 | ``` 57 | in your workspace 58 | 59 | ### Dependencies 60 | * rocpp 61 | * rospy 62 | * Python 63 | * PyGObject/GTK+3 64 | 65 | Currently only tested on GNU/Linux. Some stuff will definitely only work on 66 | Linux (e.g., the process memory, CPU statistics). 67 | 68 | ### Debugging 69 | 70 | To view debug output, start deputies with 71 | 72 | ``` 73 | ROSCONSOLE_CONFIG_FILE=`rospack find procman_ros`/config/rosconsole/debug.config rosrun procman_ros deputy -i localhost -v 74 | ``` 75 | 76 | ### Documentation 77 | 78 | Documentation is built with Doxygen. 79 | 80 | ``` 81 | cd doc 82 | doxygen 83 | ``` 84 | ## Credits 85 | - [Original program](https://github.com/ashuang/procman) by [Albert Huang](https://github.com/ashuang). 86 | - Catkinization and porting from LCM to ROS by [Marco Camurri](https://github.com/mcamurri) and [Michal Staniaszek](https://github.com/heuristicus) 87 | - Porting to PyGObject/GTK+3 by [Michal Staniaszek](https://github.com/heuristicus) 88 | 89 | ## License 90 | This software is released under the BSD-3 Software License. See the LICENSE for more details. 91 | -------------------------------------------------------------------------------- /config/rosconsole/debug.config: -------------------------------------------------------------------------------- 1 | log4j.logger.ros.procman_ros=DEBUG -------------------------------------------------------------------------------- /doc/Doxyfile: -------------------------------------------------------------------------------- 1 | OUTPUT_DIRECTORY= 2 | PROJECT_NAME=procman 3 | GENERATE_LATEX=no 4 | #TAGFILES= 5 | #GENERATE_TAGFILE= 6 | #STRIP_FROM_PATH= 7 | JAVADOC_AUTOBRIEF=yes 8 | EXTERNAL_GROUPS=no 9 | 10 | INHERIT_DOCS = YES 11 | LAYOUT_FILE = DoxygenLayout.xml 12 | 13 | INPUT = content ../python/src/procman/ 14 | FILE_PATTERNS = *.hpp *.md sheriff.py sheriff_script.py 15 | EXCLUDE_PATTERNS = 16 | EXCLUDE_SYMBOLS = ScriptExecutionContext \ 17 | SMSheriffListener \ 18 | RunScriptAction \ 19 | WaitMsAction \ 20 | WaitStatusAction \ 21 | StartStopRestartAction 22 | 23 | EXAMPLE_PATH = ../lcmtypes 24 | EXAMPLE_PATTERNS = *.lcm 25 | 26 | IMAGE_PATH = images 27 | INPUT_FILTER = 28 | FILTER_PATTERNS = *.py=./doxypypy.py 29 | -------------------------------------------------------------------------------- /doc/DoxygenLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /doc/content/comms.md: -------------------------------------------------------------------------------- 1 | Procman communications{#procman_comms} 2 | ============ 3 | 4 | [TOC] 5 | 6 | # Overview {#procman_comms_overview} 7 | 8 | Sheriffs and deputies communicate by transmitting 9 | LCM messages to each other. Together, the LCM 10 | messages define the Procman communications protocol. LCM 11 | (http://lcm-proj.github.io) is a publish/subscribe message passing system that 12 | typically uses UDP multicast as its underlying transport. It may be configured 13 | to use other types of transport as well, such as TCP. 14 | 15 | Deputies periodically (1 Hz) transmit their current status, which includes: 16 | - A listing of each command managed by the deputy and. For each command: 17 | - Whether the command is running. 18 | - The OS-assigned process ID of the command, if it's running. 19 | - How much CPU and memory are used by the command. 20 | - Which group the command is in. 21 | 22 | In addition to the deputy state, each deputy also captures the standard output 23 | and standard error for each running command, and transmits the output over LCM. 24 | 25 | Sheriffs transmit the desired state for each deputy. For each deputy, the 26 | sheriff periodically (1 Hz) transmits a message containing: 27 | - A listing of all commands the deputy _should_ be managing and the desired 28 | state for each command: 29 | - If the command should be running, stopped, or restarted 30 | - The program to run, command line arguments, and environment variables. 31 | - Which group the command should be in. 32 | - If the command should be automatically restarted if/when it terminates. 33 | 34 | The communications protocol is stateless by design: every message transmitted 35 | from a sheriff to the deputy contains the entire desired state of the deputy. 36 | Similarly, every message transmitted by a deputy contains its entire internal 37 | state. This stateless protocol has a few key features: 38 | - The entire system is robust to communication dropouts. 39 | If a deputy stops receiving messages from the sheriff, the deputy simply 40 | continues carrying out its last orders. 41 | - When the sheriff starts up, it seamlessly picks up the entire state of the 42 | system as it receives updates from each deputy. Deputies are 43 | unaffected by a sheriff starting up until the sheriff begins transmitting 44 | orders. 45 | - A sheriff in observer mode can observe the state of the system 46 | simply by receiving deputy messages and not transmitting anything. 47 | 48 | The following sections describe the messages transmitted and received by 49 | deputies and sheriffs in more detail. 50 | 51 | # Deputy {#procman_comms_deputy} 52 | 53 | ## Messages transmitted by deputy {#procman_comms_deputy_transmitted} 54 | 55 | The deputy transmits the following messages: 56 | 57 | LCM Channel | Message type | Description 58 | ------------|--------------|------------- 59 | `PM_INFO` | [deputy_info_t](\ref procman_lcm_deputy_info_t) | Summarizes the state of all commands managed by a deputy. Transmitted at 1 Hz or when a command status changes. 60 | `PM_OUTPUT` | [output_t](\ref procman_lcm_output_t) | Published when a hosted command writes output to stdout or stderr, and contains the output produced by the command. 61 | `PM_DISCOVER` | [discovery_t](\ref procman_lcm_discovery_t) | Published to check for conflicting deputies with the same ID. Published when a deputy first starts up. 62 | 63 | ## Messages received by deputy {#procman_comms_deputy_received} 64 | 65 | The deputy subscribes to the following messages: 66 | 67 | LCM Channel | Message type | Description 68 | ------------|--------------|------------- 69 | `PM_DISOCVER` | [discovery_t](\ref procman_lcm_discovery_t) | When received, the deputy replies by transmitting its state on channel `PM_INFO`. 70 | `PM_ORDERS` | [orders_t](\ref procman_lcm_orders_t) | When received, the deputy adds/removes/starts/stops commands as indicated by the orders. There are some basic checks to ensure that orders are actually targeted for the deputy. 71 | 72 | # Sheriff {#procman_comms_sheriff} 73 | 74 | ## Messages transmitted by sheriff {#procman_comms_sheriff_transmitted} 75 | 76 | LCM Channel | Message type | Description 77 | ------------|--------------|------------ 78 | `PM_ORDERS` | [orders_t](\ref procman_lcm_orders_t) | Commands a deputy. Each orders message contains the desired state of all commands managed by a single deputy. To command multiple deputies, the sheriff sends one orders message to each deputy. Transmitted at 1 Hz or when a command's desired status changes. 79 | `PM_DISCOVER` | [discovery_t](\ref procman_lcm_discovery_t) | Published by a sheriff to discover deputies when the sheriff first starts up. 80 | 81 | ## Messages received by sheriff {#procman_comms_sheriff_received} 82 | 83 | LCM Channel | Message type | Description 84 | ------------|--------------|------------ 85 | `PM_INFO` | [deputy_info_t](\ref procman_lcm_deputy_info_t) | When received, the sheriff updates its internal representation of a deputy's actual state. 86 | `PM_OUTPUT` | [output_t](\ref procman_lcm_output_t) | Contains the console output produced by a deputy-managed command. When received, the sheriff may display a command's output to the user. Subscribing to this channel is optional for a sheriff. 87 | 88 | 89 | # Appendix - message definitions {#procman_lcm_message_defs} 90 | 91 | ## procman_lcm.deputy_info_t {#procman_lcm_deputy_info_t} 92 | \include procman_lcm_deputy_info_t.lcm 93 | 94 | ## procman_lcm.cmd_status_t {#procman_lcm_cmd_status_t} 95 | \include procman_lcm_cmd_status_t.lcm 96 | 97 | ## procman_lcm.cmd_t {#procman_lcm_cmd_t} 98 | \include procman_lcm_cmd_t.lcm 99 | 100 | ## procman_lcm.output_t {#procman_lcm_output_t} 101 | \include procman_lcm_output_t.lcm 102 | 103 | ## procman_lcm.discovery_t {#procman_lcm_discovery_t} 104 | \include procman_lcm_discovery_t.lcm 105 | 106 | ## procman_lcm.orders_t {#procman_lcm_orders_t} 107 | \include procman_lcm_orders_t.lcm 108 | -------------------------------------------------------------------------------- /doc/content/config_file.md: -------------------------------------------------------------------------------- 1 | Configuration files {#procman_config_file} 2 | =================== 3 | 4 | [TOC] 5 | 6 | # Overview {#procman_config_file_overview} 7 | 8 | When working within a single project, you'll typically want to load the same 9 | set of commands into procman for managing. The sheriff configuration files 10 | provide a mechanism for: 11 | - storing a set of commands that can be loaded into procman sheriff. 12 | - writing simple scripts to sequence starting and stopping commands. 13 | 14 | A simple configuration file might look like: 15 | 16 | \code 17 | # a hash mark indicates a comment. 18 | 19 | # commands can be listed individually. This command will have ID "web-browser". 20 | cmd "web-browser" { 21 | # When started from procman, the executable named "firefox" is invoked. 22 | exec = "firefox"; 23 | # The command is assigned to the deputy specified by the "deputy" attribute. 24 | deputy = "deputy_id"; 25 | } 26 | 27 | # Commands can be grouped. This group is named "shells" 28 | group "shells" { 29 | cmd "terminal 1" { 30 | exec = "xterm"; 31 | deputy = "deputy_id"; 32 | } 33 | cmd "terminal 2" { 34 | exec = "rxvt"; 35 | deputy = "deputy_id"; 36 | } 37 | # more commands in "group_name_0" here. 38 | } 39 | # more groups... 40 | \endcode 41 | `simple_config.procman` 42 | 43 | # Loading a configuration file {#procman_config_file_loading} 44 | 45 | You can load this configuration file into procman by passing it as a command 46 | line argument: 47 | \code 48 | $ procman-sheriff simple_config.procman 49 | \endcode 50 | 51 | Additionally, the procman sheriff GUI provides a menu option to save and load 52 | configuration files. 53 | 54 | \note The procman sheriff GUI config file save operation does not preserve 55 | comments. So if you load a config file that you manually entered comments 56 | into, and then save it out again, you will lose the comments. If you want to 57 | preserve comments, then you need to stick with hand-edited config files. 58 | 59 | # Configuration file structure {#procman_config_file_structure} 60 | 61 | The general structure of a configuration file takes the form of a list of 62 | elements. Each element can be: 63 | - A command, indicated by "cmd". 64 | - A group, indicated by "group". 65 | - A script, indicated by "script". 66 | 67 | # Commands {#procman_config_file_commands} 68 | 69 | Commands are the central concept in procman, and correspond to what you might 70 | type into a shell to start a process running. A command starts with the 71 | keyword "cmd" followed by the command ID, and is described using a list of 72 | attributes surrounded by curly braces. Each attribute be expressed as a 73 | separate line of the form: 74 | \code 75 | attribute = value; 76 | \endcode 77 | 78 | Values are either quoted strings, or integers. 79 | 80 | - "exec" 81 | - String. The name of the command that will be run when the command is started. This 82 | should follow bash-style shell syntax. This attribute is __required__. 83 | - "deputy" 84 | - String. The id of the deputy that the command will be assigned to. This 85 | attribute is __required__. 86 | - "auto_respawn" 87 | - String. Must be either "true" or "false". If true, then a deputy will 88 | automatically try to restart the command if it stops, as long as the 89 | desired command status is set to running. In other words, if you order the 90 | command to stop via the sheriff, then it will not be automatically restarted. 91 | If not specified, defaults to "false". 92 | - "stop_signal" 93 | - Integer. When ordering a command to stop, this specifies the numerical 94 | OS-level signal to send the command to request a clean exit. Most of the 95 | time, this should be either 2 or 15 (corresponding to SIGINT and SIGTERM on 96 | POSIX systems). If the command does not exit after an alloted amount of 97 | time passes, then it is sent a SIGKILL. If not specified, this defaults to 98 | 2 (SIGINT). 99 | - "stop_time_allowed" 100 | - Integer. When ordering a command to stop, a deputy first sends 101 | `stop_signal` and waits `stop_time_allowed` seconds for the command to exit. 102 | If it is still running after `stop_time_allowed` seconds elapses, then the 103 | command is immediately sent a SIGKILL. If not specified, this defaults to 104 | 7. 105 | 106 | Some examples: 107 | \code 108 | cmd "MATLAB" { 109 | # Start MATLAB and have it run the script 'run_my_robot.m' 110 | exec = "matlab -nodisplay -r run_my_robot"; 111 | 112 | # Run it on deputy_id 113 | deputy = "deputy_id"; 114 | 115 | # MATLAB may not exit on SIGINT, but does respond to SIGTERM. 116 | stop_signal = 15; 117 | 118 | # may need a little extra time to exit cleanly.. 119 | stop_time_allowed = 20; 120 | } 121 | cmd "hardware interface" { 122 | exec = "hardware_driver"; 123 | deputy = "deputy_id"; 124 | # Automatically restart this command if it crashes. 125 | auto_respawn = "true"; 126 | } 127 | \endcode 128 | 129 | ## Environment variables. {#procman_config_file_commands_environment_variables} 130 | 131 | Procman supports setting and using environment variables in the executable 132 | specification for a command. Procman generally follows bash-style syntax. In 133 | particular, environment variables can be set by prefixing the command with 134 | "NAME=VALUE" pairs. There cannot be any spaces surrounding the "=" symbol in 135 | the environment variable specifications. For example: 136 | 137 | \code 138 | cmd "terminal" { 139 | # runs 'xterm' and set the environment variable 'DISPLAY' to the value ':1.0' 140 | exec = "DISPLAY=:1.0 xterm"; 141 | deputy = "deputy_id"; 142 | } 143 | \endcode 144 | 145 | In addition, environment variables can be referenced from an executable 146 | specification. Note that environment variables are always evaluated in the 147 | deputy process at the time a command is started, and not on the sheriff. For example: 148 | 149 | \code 150 | cmd "list home directory" { 151 | exec = "ls ${HOME}"; 152 | deputy = "deputy_id"; 153 | } 154 | \endcode 155 | 156 | # Groups {#procman_config_file_groups} 157 | 158 | In the same way that commands can be grouped together in the procman sheriff 159 | GUI, they can also be grouped together in configuration files using the 'group' 160 | specifier. This generally takes the form of: 161 | 162 | \code 163 | group "group_name" { 164 | # Command specifiers... 165 | # Every command enclosed by the surrounding curly braces will be treated as 166 | # part of this group. 167 | } 168 | \endcode 169 | 170 | Groups can also be nested: 171 | \code 172 | group "group_name" { 173 | group "subgroup_a" { 174 | group "subsubgroup_0" { 175 | # Command specifiers... 176 | cmd "terminal" { 177 | exec = "xterm"; 178 | deputy = "deputy_id"; 179 | } 180 | } 181 | cmd "web browser" { 182 | exec = "firefox"; 183 | deputy = "other_deputy"; 184 | } 185 | } 186 | } 187 | \endcode 188 | 189 | # Scripts {#procman_config_file_scripts} 190 | 191 | Procman sheriff supports a very simple scripting language that can be useful 192 | for sequencing the starting and stopping of commands. The scripting language 193 | allows you to specify a deterministic sequence of actions that are run one 194 | after the other. 195 | 196 | A configuration file with a simple script might look like: 197 | \code 198 | cmd "load configuration" { 199 | # This command loads the robot configuration, and then exits. 200 | exec = "configure_robot"; 201 | deputy = "robot"; 202 | } 203 | cmd "hardware interface" { 204 | exec = "hw_interface"; 205 | deputy = "robot"; 206 | } 207 | group "planning and perception" { 208 | cmd "planner" { 209 | exec = "planner"; 210 | deputy = "robot"; 211 | } 212 | cmd "perception" { 213 | exec = "perception"; 214 | deputy = "robot"; 215 | } 216 | } 217 | 218 | script "go" { 219 | # Start the 'load configuration' command, and then pause the script until 220 | # the command exits. 221 | start cmd "load configuration" wait "stopped"; 222 | 223 | # Now start the hardware interface processes 224 | start cmd "hardware interface"; 225 | 226 | # wait 1000 milliseconds 227 | wait ms 1000; 228 | 229 | # Start all of the commands in the group 'planning and perception' 230 | start group "planning and perception"; 231 | } 232 | \endcode 233 | 234 | This configuration file has a single script named "go". If you load the config 235 | file into the procman sheriff GUI, then you'll see "go" listed under the 236 | scripts menu, which you can then use to run the script. 237 | 238 | ## Script actions {#procman_config_file_script_actions} 239 | 240 | A script is composed of a sequence of actions. The valid actions are: 241 | ### "start" 242 | Usage: `start {cmd|group} TARGET_ID [ wait {"running","stopped"} ]` 243 | 244 | Orders a command or a group to start running. Examples: 245 | \code 246 | # Orders the command "planner" to start running. 247 | start cmd "planner"; 248 | 249 | # Orders the command "load configuration" to start running and then waits for 250 | # it to exit. Essentially, run it once through to completion. 251 | start cmd "load configuration" wait "stopped"; 252 | 253 | # Order the entire group "planning and perception" to start running 254 | start group "planning and perception"; 255 | 256 | # order the command "hw_interface" to start running and waits for it to 257 | # have actually started running (i.e., for the deputy to report that it has 258 | # started the child process). 259 | start cmd "hw_interface" wait "running"; 260 | # 261 | \endcode 262 | 263 | If "wait" is used on a group, then script execution only continues when all 264 | commands in the group achieve the specified status. A script can wait 265 | indefinitely, and does not timeout or fail. 266 | 267 | If "wait" is not specified, then script execution continues immediately. This 268 | way, it is possible to effectively order many commands and groups to start 269 | running all at once. 270 | 271 | ### "stop" 272 | Usage: `stop {cmd|group} TARGET_ID [ wait "stopped" ]` 273 | 274 | This is the opposite of "start", and orders a single command or a group of 275 | commands to stop execution. Commands that have the "auto_respawn" attribute 276 | will also be stopped and they will not be automatically respawned. 277 | 278 | Some example: 279 | \code 280 | # Order a single command to stop. 281 | stop cmd "hw_interface"; 282 | 283 | # Order an entire group to stop, and wait for all commands in the group to stop 284 | # before continuing. 285 | stop group "planning and perception" wait "stopped"; 286 | # 287 | \endcode 288 | 289 | ### "restart" 290 | Usage: `restart {cmd|group} TARGET_ID [ wait {"running", "stopped"} ]` 291 | 292 | The restart action first stops a command or group of commands, and then orders 293 | them to start. Using this script action is usually faster than using a "stop" 294 | followed by a "start", as the restart action is executed entirely by the 295 | deputy. 296 | 297 | Otherwise, the usage is similar to "start" and "stop". 298 | 299 | ### "wait ms" 300 | Usage: `wait ms MILLISECONDS` 301 | 302 | This action simply pauses script execution for the specified number of 303 | milliseconds. This can be useful if you need a quick and dirty way to wait for 304 | an external device to settle, or do not have another way of synchronizing 305 | script execution. 306 | 307 | Use this script action with caution, as it's almost always better to find a 308 | more robust way of sequencing script execution than by inserting arbitrary 309 | delays. 310 | 311 | ### "wait status" 312 | Usage: `wait {cmd|group} status {"running", "stopped"}` 313 | 314 | Waits for a single command, or a group of commands to all achieve the specified 315 | status. For example: 316 | 317 | \code 318 | # Order a bunch of commands to stop. 319 | stop cmd "abc"; 320 | stop cmd "def"; 321 | stop cmd "ghi"; 322 | # Now wait for them all to actually stop. 323 | wait cmd "abc" status "stopped"; 324 | wait cmd "def" status "stopped"; 325 | wait cmd "ghi" status "stopped"; 326 | \endcode 327 | 328 | ### "run_script" 329 | 330 | Usage: `run_script OTHER_SCRIPT_NAME` 331 | 332 | A configuration file can have multiple scripts. This script action invokes 333 | another script listed in the configuration file and waits the other script to 334 | finish execution before continuing. 335 | 336 | ## Running scripts {#procman_config_file_script_running} 337 | 338 | Scripts can be generally be run in one of several ways: 339 | - load a configuration file into the procman sheriff GUI, and select the script 340 | from the "script" menu. 341 | - pass the configuration file and script name as command line arguments to `procman-sheriff`. For example: 342 | \code 343 | $ procman-sheriff config_file.procman go 344 | \endcode 345 | 346 | ## Referencing nested groups {#procman_config_file_script_nested_groups} 347 | 348 | The syntax for identifying subgroups (i.e., groups nested within another group) 349 | is to join the group names with slashes. For example, if group "cameras" is a 350 | subgroup of group "hardware drivers", then a script would refer to the inner 351 | group by the name "hardware drivers/cameras". 352 | 353 | \code 354 | group "hardware drivers" { 355 | group "cameras" { 356 | cmd "left camera" { 357 | exec = "left_camera"; 358 | deputy = "robot"; 359 | } 360 | cmd "right camera" { 361 | exec = "right_camera"; 362 | deputy = "robot"; 363 | } 364 | } 365 | } 366 | 367 | script "go" { 368 | start group "hardware drivers/cameras"; 369 | } 370 | 371 | For this reason, slashes are not allowed in group names. 372 | \endcode 373 | 374 | -------------------------------------------------------------------------------- /doc/content/design.md: -------------------------------------------------------------------------------- 1 | Procman design overview {#procman_design} 2 | ===================== 3 | 4 | This page explains the design of procman and how the different components work 5 | together to control processes. 6 | 7 | # Deputies and Sheriffs {#procman_design_deputies_and_sheriffs} 8 | 9 | There are two types of processes in procman: deputies and sheriffs. 10 | 11 | _Deputies_ host and control processes directly. A deputy can: 12 | - start child processes 13 | - stop child processes 14 | - monitor and report the status of hosted processes 15 | 16 | The deputy is essentially a daemon process that manages other commands. It is 17 | not interactive, does not have a GUI, and simply carries out orders that it 18 | receives from a sheriff. 19 | 20 | 21 | _Sheriffs_ tell deputies what to do. A single sheriff can command many 22 | deputies. Any process can be a sheriff as long as it implements the 23 | sheriff/deputy communications protocol, but the most commonly used sheriff is 24 | the `procman-sheriff` GUI tool provided with Procman. The Procman Python API 25 | can also be used to implement a custom sheriff. 26 | 27 | Sheriffs and deputies [communicate via LCM](\ref procman_comms), a UDP 28 | multicast-based communications protocol. All communications are stateless, 29 | which enables sheriffs and deputies to work together more easily in the 30 | presence of network and communication dropouts. 31 | 32 | ## procman-sheriff {#procman_design_procman_sheriff} 33 | 34 | `procman-sheriff` is the primary implementation of Procman sheriff, and can 35 | be used to communicate with and command deputies. The sheriff forms a global 36 | view of all deputies and their commands. The sheriff sends commands to the 37 | deputies, and specifies which commands a deputy should be managing, and the 38 | desired state of those commands. 39 | 40 | The sheriff has an interactive GUI through which a user can modify commands and 41 | their desired statuses. It also has a scripting facility that can be useful 42 | for starting multiple commands at once, sequencing a startup procedure, or 43 | running simple scripts in general. 44 | 45 | The sheriff can also be run from the command line without a GUI. 46 | 47 | \image html procman-sheriff-screenshot.png "procman-sheriff screenshot" 48 | 49 | ### Observer mode {#procman_design_procman_sheriff_observer_mode} 50 | 51 | `procman-sheriff` can be switched to observer mode, where it stops 52 | transmitting commands, and simply displays the state of the deputies. Observer 53 | mode is useful in situations where you want to simply observe the state of a 54 | running system. Examples of this include situations where the active sheriff 55 | is running without a GUI, and also when replaying an LCM log file that contains 56 | deputy status message (using the LCM log playback tools). 57 | -------------------------------------------------------------------------------- /doc/content/main.md: -------------------------------------------------------------------------------- 1 | Procman {#mainpage} 2 | ======= 3 | 4 | # Introduction {#procman_introduction} 5 | 6 | Procman is a set of tools for managing multiple processes distributed across 7 | one or more hosts. 8 | 9 | A single GUI provides the following features: 10 | - start, stop, and restart processes 11 | - view process-level CPU/memory usage and console output 12 | - group processes together (e.g., the "controls" group) for organization 13 | - run simple scripts 14 | 15 | In addition to the GUI, there is also a [Python API](\ref python_api) for more programmatic 16 | control of processes. 17 | 18 | # Table of Contents {#procman_toc} 19 | 20 | - \ref procman_tutorial 21 | - [Design overview](\ref procman_design) 22 | - \ref procman_config_file 23 | - [Communications protocol](\ref procman_comms) 24 | -------------------------------------------------------------------------------- /doc/content/tutorial.md: -------------------------------------------------------------------------------- 1 | Quick Start Tutorial {#procman_tutorial} 2 | ==================== 3 | 4 | This section provides a brief introduction to Procman and controlling processes 5 | with a deputy and a sheriff. 6 | 7 | There are two types of programs in Procman - the deputy (\p procman-deputy) 8 | and the sheriff (\p procman-sheriff). 9 | 10 | - Deputies control processes, and there must be at least one deputy. 11 | - Sheriffs control deputies, and there can be at most one sheriff. 12 | 13 | To get started, we'll show you how to run the deputy and the sheriff, and create 14 | and manage commands. 15 | 16 | - \ref procman_tutorial_starting 17 | - \ref procman_tutorial_command_creation 18 | - \ref procman_tutorial_command_managing 19 | - \ref procman_tutorial_config 20 | 21 | # Starting a deputy and a sheriff {#procman_tutorial_starting} 22 | 23 | First, open up your favorite terminal program and run the deputy command: 24 | 25 | \code 26 | procman-deputy 27 | \endcode 28 | 29 | Then, in another terminal, launch the sheriff GUI. 30 | 31 | \code 32 | procman-sheriff 33 | \endcode 34 | 35 | When you run the sheriff, you should see a GUI that looks something like this: 36 | 37 | \image html sheriff-gui-empty.png "procman-sheriff with one deputy and no commands." 38 | 39 | There are three panes in the sheriff GUI: 40 | - Top left: the command pane 41 | - Top right: the deputy pane 42 | - Bottom: the console pane 43 | 44 | The _command pane_ shows all the commands managed by Procman. Since we haven't 45 | created any commands, there isn't anything to see there yet. 46 | 47 | The _deputy pane_ shows all the deputies detected by the sheriff. We've started 48 | one deputy, so it shows up in the top right. Deputies are always named, and 49 | the default name is the computer hostname where the deputy process is running. 50 | In this case, the hostname is "contact", so the deputy is named "contact" as well. 51 | 52 | The _console pane_ shows console output (stdout and stderr) from running 53 | commands, and also status information from the sheriff. 54 | 55 | # Creating a command {#procman_tutorial_command_creation} 56 | 57 | To create a command, select the menu item "Commands -> New command". 58 | 59 | \image html sheriff-gui-add-command-menu.png "Adding a command using the menu bar." 60 | 61 | Following that, a dialog box should appear with several options to fill out. 62 | 63 | \image html sheriff-gui-add-command.png "Add command dialog." 64 | 65 | The options are: 66 | - \c Deputy - which deputy should manage the command. 67 | - \c Command - the command to execute and any command-line arguments. 68 | - \c Name - a name for the command, used for display purposes and to identify the command. 69 | - \c Group - commands can be grouped together, or not. 70 | - \c Auto-restart - if checked, deputies will automatically restart commands when they terminate. 71 | 72 | In the image above, we've set the command to \c xterm, a fairly common terminal 73 | emulator. If you don't have \c xterm installed, replace the command with 74 | something you do have on your system. We've also named the command "terminal 75 | emulator". 76 | 77 | # Managing commands {#procman_tutorial_command_managing} 78 | Now that we have a command, we can run it. Click on the command in the command 79 | pane so that it is highlighted. Once the command is selected, click on the menu 80 | bar item "Commands -> Start". 81 | 82 | \image html sheriff-gui-start-command-menu.png 83 | 84 | You should now see the \c xterm terminal emulator popup, and the status of the command 85 | should change to "Running". 86 | 87 | \image html sheriff-gui-command-running.png 88 | 89 | And that's basically it. 90 | 91 | You can stop, restart, move, and edit the selected command using the 92 | "Commands" menu. 93 | 94 | There are a bunch more features to Procman, but creating, starting, and 95 | stopping processes is its core functionality. 96 | 97 | # Saving and loading configurations {#procman_tutorial_config} 98 | 99 | It's often useful to save the commands you've created so that you can easily 100 | load them and run them again later on. To do this, select "File -> Save config 101 | as", and then select a filename. 102 | 103 | The configuration files are human-readable, so if you save the configuration 104 | above and opened up the file in a text editor, you'd see something like the 105 | following: 106 | 107 | \verbatim 108 | cmd "terminal emulator" { 109 | exec = "xterm"; 110 | host = "contact"; 111 | } 112 | \endverbatim 113 | 114 | It should be pretty straightforward to figure out the format, and you can edit 115 | the configuration files directly with a text editor. For more information on 116 | the configuration file format, see the [documentation on configuration files](\ref procman_config_file). 117 | 118 | To load a configuration, click on the menu bar item "File -> load config". 119 | -------------------------------------------------------------------------------- /doc/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /doc/images/procman-sheriff-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ori-drs/procman_ros/432bfdbd2e6001775121b92ad901581ba80b09b4/doc/images/procman-sheriff-screenshot.png -------------------------------------------------------------------------------- /doc/images/procman_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ori-drs/procman_ros/432bfdbd2e6001775121b92ad901581ba80b09b4/doc/images/procman_icon.png -------------------------------------------------------------------------------- /doc/images/sheriff-gui-add-command-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ori-drs/procman_ros/432bfdbd2e6001775121b92ad901581ba80b09b4/doc/images/sheriff-gui-add-command-menu.png -------------------------------------------------------------------------------- /doc/images/sheriff-gui-add-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ori-drs/procman_ros/432bfdbd2e6001775121b92ad901581ba80b09b4/doc/images/sheriff-gui-add-command.png -------------------------------------------------------------------------------- /doc/images/sheriff-gui-command-running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ori-drs/procman_ros/432bfdbd2e6001775121b92ad901581ba80b09b4/doc/images/sheriff-gui-command-running.png -------------------------------------------------------------------------------- /doc/images/sheriff-gui-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ori-drs/procman_ros/432bfdbd2e6001775121b92ad901581ba80b09b4/doc/images/sheriff-gui-empty.png -------------------------------------------------------------------------------- /doc/images/sheriff-gui-start-command-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ori-drs/procman_ros/432bfdbd2e6001775121b92ad901581ba80b09b4/doc/images/sheriff-gui-start-command-menu.png -------------------------------------------------------------------------------- /doc/images/sheriff-gui-with-xterm-stopped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ori-drs/procman_ros/432bfdbd2e6001775121b92ad901581ba80b09b4/doc/images/sheriff-gui-with-xterm-stopped.png -------------------------------------------------------------------------------- /include/procman/exec_string_utils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PROCMAN_EXEC_STRING_UTILS_HPP__ 2 | #define PROCMAN_EXEC_STRING_UTILS_HPP__ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "procman.hpp" 9 | 10 | namespace procman { 11 | 12 | /** 13 | * Do variable expansion on a command argument. This searches the argument for 14 | * text of the form $VARNAME and ${VARNAME}. For each discovered variable, it 15 | * then expands the variable using the environment. If a variable expansion 16 | * fails, then the corresponding text is left unchanged. 17 | */ 18 | std::string ExpandVariables(const std::string& input); 19 | 20 | std::vector SeparateArgs(const std::string& input); 21 | 22 | std::vector Split(const std::string& input, 23 | const std::string& delimeters, 24 | int max_items); 25 | 26 | void Strfreev(char** vec); 27 | 28 | } // namespace procman 29 | 30 | #endif // PROCMAN_EXEC_STRING_UTILS_HPP__ 31 | -------------------------------------------------------------------------------- /include/procman/procinfo.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PROCMAN_PROCINFO_HPP__ 2 | #define PROCMAN_PROCINFO_HPP__ 3 | 4 | // functions for reading how much CPU and memory are being used by individual 5 | // processes and the system as a whole 6 | 7 | #include 8 | #include 9 | 10 | namespace procman { 11 | 12 | struct ProcessInfo { 13 | int pid; 14 | 15 | // cpu usage time 16 | uint32_t user; 17 | uint32_t system; 18 | 19 | // memory usage 20 | 21 | // VSIZE bytes 22 | int64_t vsize; 23 | 24 | // RSS bytes 25 | int64_t rss; 26 | 27 | // SHR bytes 28 | int64_t shared; 29 | 30 | int64_t text; 31 | int64_t data; 32 | }; 33 | 34 | struct SystemInfo { 35 | uint32_t user; 36 | uint32_t user_low; 37 | uint32_t system; 38 | uint32_t idle; 39 | 40 | int64_t memtotal; 41 | int64_t memfree; 42 | int64_t swaptotal; 43 | int64_t swapfree; 44 | }; 45 | 46 | bool ReadProcessInfoWithChildren(int pid, ProcessInfo *procinfo); 47 | 48 | bool ReadProcessInfo(int pid, ProcessInfo *s); 49 | 50 | bool ReadSystemInfo(SystemInfo *s); 51 | 52 | std::vector GetDescendants(int pid); 53 | 54 | bool IsOrphanedChildOf(int orphan, int parent); 55 | 56 | } // namespace procman 57 | 58 | #endif // PROCMAN_PROCINFO_HPP__ 59 | -------------------------------------------------------------------------------- /include/procman/procman.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PROCMAN_PROCMAN_HPP__ 2 | #define PROCMAN_PROCMAN_HPP__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | namespace procman { 13 | 14 | enum CommandStatus { 15 | PROCMAN_CMD_STOPPED = 0, 16 | PROCMAN_CMD_RUNNING, 17 | PROCMAN_CMD_INVALID 18 | }; 19 | 20 | class ProcmanCommand { 21 | public: 22 | ~ProcmanCommand(); 23 | 24 | const std::string& ExecStr() const { return exec_str_; } 25 | 26 | int Pid() const { return pid_; } 27 | 28 | int StdoutFd() const { return stdout_fd_; } 29 | 30 | int StdinFd() const { return stdin_fd_; } 31 | 32 | int ExitStatus() const { return exit_status_; } 33 | 34 | private: 35 | ProcmanCommand(const std::string& exec_str); 36 | 37 | void SetPid(int pid) { pid_ = pid; } 38 | 39 | void SetStdinFd(int fd) { stdin_fd_ = fd; } 40 | 41 | void SetStdoutFd(int fd) { stdout_fd_ = fd; } 42 | 43 | void SetExitStatus(int status) { exit_status_ = status; } 44 | 45 | void PrepareArgsAndEnvironment(); 46 | 47 | friend class Procman; 48 | 49 | // the command to execute. 50 | std::string exec_str_; 51 | 52 | // pid of process when running. 0 otherwise 53 | int pid_; 54 | 55 | // when the process is running, writing to this pipe 56 | // writes to stdin of the process 57 | int stdin_fd_; 58 | 59 | // and reading from this pipe reads from stdout of the proc 60 | int stdout_fd_; 61 | 62 | int exit_status_; 63 | 64 | // number of arguments 65 | int argc_; 66 | char **argv_; 67 | 68 | // environment variables to set 69 | std::map environment_; 70 | 71 | std::vector descendants_to_kill_; // Used internally when killing a process. 72 | }; 73 | 74 | typedef std::shared_ptr ProcmanCommandPtr; 75 | 76 | class Procman { 77 | public: 78 | Procman(); 79 | 80 | /** 81 | * Destructor. 82 | * 83 | * On desctruction, calls RemoveCommand() on all commands. 84 | */ 85 | ~Procman(); 86 | 87 | const std::vector& GetCommands(); 88 | 89 | /** 90 | * Starts a command running. 91 | */ 92 | void StartCommand(ProcmanCommandPtr cmd); 93 | 94 | /** 95 | * Sends the specified POSIX signal to a command. 96 | */ 97 | bool KillCommand(ProcmanCommandPtr cmd, int signum); 98 | 99 | /** 100 | * Adds a command to be managed by procman. returns a pointer to a newly 101 | * created ProcmanCommand, or NULL on failure 102 | * 103 | * The command is not started. To start a command running, use 104 | * procman_start_cmd 105 | */ 106 | ProcmanCommandPtr AddCommand(const std::string& exec_str); 107 | 108 | /** 109 | * Removes a command from management by procman. 110 | * 111 | * If the command is not already stopped, then RemoveCommand() blocks and 112 | * waits for the command to stop running. RemoveCommand() does _not_ try to 113 | * actively stop the command by sending it a signal or anything like that. 114 | */ 115 | void RemoveCommand(ProcmanCommandPtr cmd); 116 | 117 | /** 118 | * Checks to see if any processes have stopped running. 119 | * 120 | * If a child process has died, then a pointer to the dead command is 121 | * returned. Otherwise, an empty pointer is returned. 122 | * 123 | * This function does not block. 124 | */ 125 | ProcmanCommandPtr CheckForStoppedCommands(); 126 | 127 | /** 128 | * Cleans up resources used by the stopped command. 129 | * 130 | * Call this after a command has terminated but you don't want to remove 131 | * it (e.g., it might be started again later). 132 | * 133 | * This method is automatically called by RemoveCommand(), so if you're 134 | * removing a command then there's no need to explicitly call 135 | * CleanupStoppedCommand(). 136 | */ 137 | void CleanupStoppedCommand(ProcmanCommandPtr cmd); 138 | 139 | CommandStatus GetCommandStatus(ProcmanCommandPtr cmd); 140 | 141 | /* Changes the command that will be executed. 142 | * 143 | * This has no effect until the command is next started. 144 | */ 145 | void SetCommandExecStr(ProcmanCommandPtr cmd, const std::string& exec_str); 146 | 147 | private: 148 | void CheckCommand(ProcmanCommandPtr cmd); 149 | std::vector commands_; 150 | std::vector dead_children_; 151 | }; 152 | 153 | } // namespace procman 154 | 155 | #endif // PROCMAN_PROCMAN_HPP__ 156 | -------------------------------------------------------------------------------- /include/procman_ros/procman_deputy.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PROCMAN_PROCMAN_DEPUTY_HPP__ 2 | #define PROCMAN_PROCMAN_DEPUTY_HPP__ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "procman_ros/socket_monitor.hpp" 9 | #include "procman/procman.hpp" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "std_msgs/String.h" 15 | 16 | namespace procman { 17 | 18 | struct DeputyCommand; 19 | 20 | struct DeputyOptions { 21 | static DeputyOptions Defaults(); 22 | 23 | std::string deputy_id; 24 | bool verbose; 25 | }; 26 | 27 | class ProcmanDeputy { 28 | public: 29 | ProcmanDeputy(const DeputyOptions& options); 30 | ~ProcmanDeputy(); 31 | 32 | void Run(); 33 | 34 | private: 35 | void OrdersReceived(const procman_ros::ProcmanOrdersConstPtr& orders); 36 | void DiscoveryReceived(const procman_ros::ProcmanDiscoveryConstPtr& msg); 37 | void InfoReceived(const procman_ros::ProcmanDeputyInfoConstPtr& msg); 38 | 39 | void OnDiscoveryTimer(const ros::WallTimerEvent& event); 40 | 41 | void OnOneSecondTimer(const ros::WallTimerEvent& event); 42 | 43 | void OnIntrospectionTimer(const ros::WallTimerEvent& event); 44 | 45 | void OnQuitTimer(const ros::WallTimerEvent& event); 46 | 47 | void OnPosixSignal(int signum); 48 | 49 | void OnProcessOutputAvailable(DeputyCommand* deputy_cmd); 50 | 51 | void UpdateCpuTimes(); 52 | 53 | void CheckForStoppedCommands(); 54 | 55 | void TransmitProcessInfo(); 56 | 57 | void MaybeScheduleRespawn(DeputyCommand *deputy_cmd); 58 | 59 | int StartCommand(DeputyCommand* deputy_cmd, int desired_runid); 60 | 61 | int StopCommand(DeputyCommand* deputy_cmd); 62 | 63 | void TransmitStr(const std::string& command_id, const char* str); 64 | 65 | void PrintfAndTransmit(const std::string& command_id, const char *fmt, ...); 66 | 67 | void MaybePublishOutputMessage(const ros::WallTimerEvent& event); 68 | 69 | void ProcessSockets(); 70 | 71 | DeputyOptions options_; 72 | 73 | Procman* pm_; 74 | 75 | SocketMonitor event_loop_; 76 | 77 | std::string deputy_id_; 78 | 79 | SystemInfo current_system_status; 80 | SystemInfo previous_system_status; 81 | float cpu_load_; 82 | 83 | int64_t deputy_start_time_; 84 | pid_t deputy_pid_; 85 | 86 | ros::Subscriber discovery_sub_; 87 | ros::Subscriber info_sub_; 88 | ros::Subscriber orders_sub_; 89 | 90 | ros::Publisher info_pub_; 91 | ros::Publisher discover_pub_; 92 | ros::Publisher output_pub_; 93 | 94 | 95 | ros::NodeHandle nh_; 96 | 97 | ros::WallTimer discovery_timer_; 98 | ros::WallTimer one_second_timer_; 99 | ros::WallTimer introspection_timer_; 100 | ros::WallTimer quit_timer_; 101 | ros::WallTimer check_output_msg_timer_; 102 | 103 | std::map commands_; 104 | 105 | bool exiting_; 106 | 107 | int64_t last_output_transmit_utime_; 108 | int output_buf_size_; 109 | procman_ros::ProcmanOutput output_msg_; 110 | }; 111 | 112 | } // namespace procman 113 | 114 | #endif // PROCMAN_PROCMAN_DEPUTY_HPP__ 115 | -------------------------------------------------------------------------------- /include/procman_ros/socket_monitor.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PROCMAN_EVENT_LOOP_HPP__ 2 | #define PROCMAN_EVENT_LOOP_HPP__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace procman { 10 | 11 | class SocketMontior; 12 | 13 | class SocketNotifier; 14 | 15 | 16 | typedef std::shared_ptr SocketNotifierPtr; 17 | 18 | class SocketMonitor { 19 | public: 20 | enum EventType { 21 | kRead, 22 | kWrite, 23 | kError 24 | }; 25 | 26 | SocketMonitor(); 27 | 28 | ~SocketMonitor(); 29 | 30 | SocketNotifierPtr AddSocket(int fd, EventType event_type, 31 | std::function callback); 32 | 33 | void SetPosixSignals(const std::vector& signums, 34 | std::function callback); 35 | 36 | void Run(); 37 | 38 | void Quit(); 39 | 40 | bool isQuitting() const { 41 | return quit_; 42 | } 43 | 44 | void IterateOnce(); 45 | 46 | private: 47 | friend class SocketNotifier; 48 | 49 | bool quit_; 50 | 51 | std::vector sockets_; 52 | 53 | std::vector sockets_ready_; 54 | 55 | SocketNotifierPtr posix_signal_notifier_; 56 | }; 57 | 58 | } // namespace procman 59 | 60 | #endif // PROCMAN_EVENT_LOOP_HPP__ 61 | -------------------------------------------------------------------------------- /msg/ProcmanCmd.msg: -------------------------------------------------------------------------------- 1 | # Uniquely identifies the command 2 | string command_id 3 | 4 | # Executable string. 5 | string exec_str 6 | 7 | # Logical group that the command belongs to. 8 | string group 9 | 10 | # If true and the command stops running on its own, then the deputy will 11 | # automatically restart the command. 12 | bool auto_respawn 13 | 14 | # The POSIX signal number to send when stopping the command. Typically 15 | # this is something like SIGINT or SIGTERM 16 | int8 stop_signal 17 | 18 | # How much time (seconds) to allow the command to gracefully terminate, 19 | # when stopping the command, before issuing a SIGKILL. 20 | float32 stop_time_allowed 21 | 22 | -------------------------------------------------------------------------------- /msg/ProcmanCmdDesired.msg: -------------------------------------------------------------------------------- 1 | # describes the state of a command managed by the procman sheriff/deputy 2 | # ============ set by the sheriff ========== 3 | 4 | # describes the command 5 | ProcmanCmd cmd 6 | 7 | # to start a command running, the sheriff should change desired_runid and 8 | # unset force_quit 9 | int32 desired_runid 10 | 11 | # flag set by the sheriff. If set, then the deputy should forcefully 12 | # terminate the command 13 | bool force_quit 14 | 15 | -------------------------------------------------------------------------------- /msg/ProcmanCmdStatus.msg: -------------------------------------------------------------------------------- 1 | # describes the state of a command managed by the procman sheriff/deputy 2 | 3 | # The command to execute. 4 | ProcmanCmd cmd 5 | 6 | # If the command is running, then this is the pid if not, then this is 7 | # zero. 8 | int32 pid 9 | 10 | # an ID for the run instance 11 | int32 actual_runid 12 | 13 | # the last exit code 14 | int32 exit_code 15 | 16 | # [0, 1] 17 | float32 cpu_usage 18 | 19 | # total virtual memory used by the process 20 | int64 mem_vsize_bytes 21 | 22 | # total physical memory used by the process 23 | int64 mem_rss_bytes 24 | 25 | -------------------------------------------------------------------------------- /msg/ProcmanDeputyInfo.msg: -------------------------------------------------------------------------------- 1 | # message sent by a procman deputy, primarily intended for the procman 2 | # sheriff. informs the sheriff of the status of processes running on the 3 | # host managed by the deputy. 4 | 5 | time timestamp 6 | string deputy_id 7 | 8 | # [0, 1] 9 | float32 cpu_load 10 | 11 | int64 phys_mem_total_bytes 12 | int64 phys_mem_free_bytes 13 | int64 swap_total_bytes 14 | int64 swap_free_bytes 15 | 16 | int32 ncmds 17 | ProcmanCmdStatus[] cmds 18 | 19 | -------------------------------------------------------------------------------- /msg/ProcmanDiscovery.msg: -------------------------------------------------------------------------------- 1 | # message sent by a procman deputy or sheriff to discover other deputies and 2 | # sheriffs on the network. 3 | 4 | time timestamp 5 | 6 | # Id of the transmitter. If the tranmsitter is a deputy, then this is the 7 | # deputy id. If the transmitter is a sheriff, then this is the sheriff id. 8 | string transmitter_id 9 | 10 | int64 nonce 11 | 12 | -------------------------------------------------------------------------------- /msg/ProcmanOrders.msg: -------------------------------------------------------------------------------- 1 | # message sent by the procman sheriff to provide instructions for a procman 2 | # deputy. 3 | 4 | time timestamp 5 | string deputy_id 6 | string sheriff_id 7 | 8 | int32 ncmds 9 | ProcmanCmdDesired[] cmds 10 | 11 | -------------------------------------------------------------------------------- /msg/ProcmanOutput.msg: -------------------------------------------------------------------------------- 1 | # Sent by a procman deputy when one or ore child processes write something to 2 | # stdout/stderr. 3 | 4 | # Output from multiple processes can be contained in a single message. This 5 | # can be done to reduce the number of individual packets transmitted by the 6 | # deputy. 7 | 8 | # Also sent when the deputy itself has something to say. 9 | 10 | time timestamp 11 | 12 | # Which deputy sent this message. 13 | string deputy_id 14 | 15 | # size of the arrays of commands/text 16 | int16 num_commands 17 | 18 | # Id of each child process. Empty string if the message is generated by 19 | # the deputy 20 | string[] command_ids 21 | 22 | string[] text 23 | 24 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | procman_ros 3 | 0.1.7 4 | Procman-ros is a tool for managing many processes distributed over one or more computers using ROS message passing. It is a fork of github.com/ashuang/procman. 5 | Marco Camurri 6 | Michal Staniaszek 7 | BSD-3 8 | catkin 9 | rospy 10 | roscpp 11 | std_msgs 12 | message_generation 13 | message_runtime 14 | -------------------------------------------------------------------------------- /python/procman-ros-sheriff.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Procman Sheriff 7 | 800 8 | 600 9 | 10 | 11 | 12 | 13 | True 14 | 15 | 16 | True 17 | 18 | 19 | True 20 | _File 21 | True 22 | 23 | 24 | True 25 | 26 | 27 | True 28 | _Load config 29 | True 30 | 31 | 32 | 33 | 34 | 35 | 36 | True 37 | _Save config as 38 | True 39 | 40 | 41 | 42 | 43 | 44 | 45 | True 46 | _Preferences 47 | True 48 | 49 | 50 | 51 | 52 | 53 | True 54 | 55 | 56 | 57 | 58 | gtk-quit 59 | True 60 | True 61 | True 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | True 73 | _Options 74 | True 75 | 76 | 77 | True 78 | 79 | 80 | True 81 | _Observer 82 | True 83 | 84 | 85 | 86 | 87 | 88 | True 89 | _Spawn local deputy 90 | True 91 | 92 | 93 | 94 | 95 | 96 | True 97 | False 98 | _Terminate local deputy 99 | True 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | True 110 | _Commands 111 | True 112 | 113 | 114 | True 115 | 116 | 117 | True 118 | False 119 | _Start 120 | True 121 | 122 | 123 | 124 | 125 | 126 | 127 | True 128 | False 129 | S_top 130 | True 131 | 132 | 133 | 134 | 135 | 136 | 137 | True 138 | False 139 | _Restart 140 | True 141 | 142 | 143 | 144 | 145 | 146 | 147 | True 148 | False 149 | Remo_ve 150 | True 151 | 152 | 153 | 154 | 155 | 156 | 157 | True 158 | 159 | 160 | 161 | 162 | True 163 | False 164 | _Edit command 165 | True 166 | 167 | 168 | 169 | 170 | 171 | 172 | True 173 | _New command 174 | True 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | True 186 | _View 187 | True 188 | 189 | 190 | True 191 | 192 | 193 | 194 | 195 | 196 | 197 | True 198 | _Scripts 199 | True 200 | 201 | 202 | True 203 | 204 | 205 | True 206 | 207 | 208 | 209 | 210 | True 211 | New script 212 | 213 | 214 | 215 | 216 | 217 | True 218 | False 219 | Edit script 220 | 221 | 222 | True 223 | 224 | 225 | 226 | 227 | 228 | 229 | True 230 | False 231 | Remove script 232 | 233 | 234 | True 235 | 236 | 237 | 238 | 239 | 240 | 241 | True 242 | 243 | 244 | 245 | 246 | True 247 | False 248 | Abort script 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | False 259 | 0 260 | 261 | 262 | 263 | 264 | True 265 | True 266 | 300 267 | True 268 | 269 | 270 | True 271 | True 272 | 500 273 | True 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | False 283 | True 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 1 292 | 293 | 294 | 295 | 296 | True 297 | 2 298 | 299 | 300 | False 301 | 2 302 | 303 | 304 | 305 | 306 | 307 | 308 | -------------------------------------------------------------------------------- /python/src/procman_ros/__init__.py: -------------------------------------------------------------------------------- 1 | from .sheriff import Sheriff 2 | -------------------------------------------------------------------------------- /python/src/procman_ros/sheriff_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import os 5 | import signal 6 | import subprocess 7 | import sys 8 | import time 9 | 10 | from procman_ros.sheriff_script import ScriptManager, ScriptListener 11 | from procman_ros.sheriff import Sheriff 12 | import procman_ros.sheriff as sheriff 13 | 14 | 15 | class SheriffHeadless(ScriptListener): 16 | def __init__(self, config, spawn_deputy, script_name, script_done_action): 17 | self.sheriff = Sheriff() 18 | self.script_manager = ScriptManager(self.sheriff) 19 | self.spawn_deputy = spawn_deputy 20 | self.spawned_deputy = None 21 | self.config = config 22 | self.script_name = script_name 23 | self.script = None 24 | self.mainloop = None 25 | self._should_exit = False 26 | if script_done_action is None: 27 | self.script_done_action = "exit" 28 | else: 29 | self.script_done_action = script_done_action 30 | 31 | def _shutdown(self): 32 | if self.spawned_deputy: 33 | print("Terminating local deputy..") 34 | try: 35 | self.spawned_deputy.terminate() 36 | except AttributeError: 37 | os.kill(self.spawned_deputy.pid, signal.SIGTERM) 38 | self.spawned_deputy.wait() 39 | self.spawned_deputy = None 40 | self.sheriff.shutdown() 41 | self.script_manager.shutdown() 42 | 43 | def _start_script(self): 44 | if not self.script: 45 | return False 46 | print("Running script {}".format(self.script_name)) 47 | errors = self.script_manager.execute_script(self.script) 48 | if errors: 49 | print("Script failed to run. Errors detected:\n" + "\n".join(errors)) 50 | self._shutdown() 51 | sys.exit(1) 52 | return False 53 | 54 | def script_finished(self, script_object): 55 | # Overriden from ScriptListener. Called by ScriptManager when a 56 | # script is finished. 57 | if self.script_done_action == "exit": 58 | self._request_exit() 59 | elif self.script_done_action == "observe": 60 | self.sheriff.set_observer(True) 61 | 62 | def _request_exit(self): 63 | self._should_exit = True 64 | 65 | def run(self): 66 | # parse the config file 67 | if self.config is not None: 68 | self.sheriff.load_config(self.config) 69 | self.script_manager.load_config(self.config) 70 | 71 | # start a local deputy? 72 | if self.spawn_deputy: 73 | args = ["rosrun", "procman_ros", "deputy", "-i", "localhost"] 74 | self.spawned_deputy = subprocess.Popen(args) 75 | else: 76 | self.spawned_deputy = None 77 | 78 | # run a script 79 | if self.script_name: 80 | self.script = self.script_manager.get_script(self.script_name) 81 | if not self.script: 82 | print("No such script: {}".format(self.script_name)) 83 | self._shutdown() 84 | sys.exit(1) 85 | errors = self.script_manager.check_script_for_errors(self.script) 86 | if errors: 87 | print("Unable to run script. Errors were detected:\n\n") 88 | print("\n ".join(errors)) 89 | self._shutdown() 90 | sys.exit(1) 91 | 92 | self.script_manager.add_listener(self) 93 | 94 | signal.signal(signal.SIGINT, lambda *_: self._request_exit()) 95 | signal.signal(signal.SIGTERM, lambda *_: self._request_exit()) 96 | signal.signal(signal.SIGHUP, lambda *_: self._request_exit()) 97 | 98 | try: 99 | if self.script: 100 | time.sleep(0.2) 101 | self._start_script() 102 | while self.script_manager._active_script_context is not None: 103 | time.sleep(0.2) 104 | except KeyboardInterrupt: 105 | pass 106 | except OSError: 107 | pass 108 | finally: 109 | print("Sheriff terminating..") 110 | self._shutdown() 111 | 112 | return 0 113 | 114 | 115 | def main(): 116 | parser = argparse.ArgumentParser( 117 | description="Process management operating console.", 118 | epilog="If procman_config_file is specified, then the sheriff tries to load " 119 | "deputy commands from the file.\n\nIf script_name is additionally " 120 | "specified, then the sheriff executes the named script once the config " 121 | "file is loaded.", 122 | formatter_class=argparse.RawDescriptionHelpFormatter, 123 | ) 124 | 125 | if "-o" not in sys.argv: 126 | parser.add_argument("procman_config_file", help="The configuration file to load") 127 | 128 | parser.add_argument( 129 | "--script", help="A script to execute after the config file is loaded." 130 | ) 131 | 132 | mode = parser.add_mutually_exclusive_group() 133 | mode.add_argument( 134 | "-l", 135 | "--lone-ranger", 136 | action="store_true", 137 | dest="spawn_deputy", 138 | help="Automatically run a deputy within the sheriff process. This deputy terminates with the " 139 | "sheriff, along with all the commands it hosts.", 140 | ) 141 | mode.add_argument( 142 | "-o", 143 | "--observer", 144 | action="store_true", 145 | help="Runs in observer mode on startup. This " 146 | "prevents the sheriff from sending any " 147 | "commands, and is useful for monitoring " 148 | "existing procman_ros sheriff and/or deputy " 149 | "instances.", 150 | ) 151 | parser.add_argument( 152 | "--on-script-complete", 153 | choices=["exit", "observe"], 154 | dest="script_done_action", 155 | help='Only valid if a script is specified. If set to "exit", then the sheriff exits when ' 156 | 'the script is done executing. If set to "observe", then the sheriff self-demotes to ' 157 | "observer mode.", 158 | ) 159 | 160 | args = parser.parse_args(sys.argv[1:]) 161 | 162 | if hasattr(args, "procman_config_file"): 163 | try: 164 | cfg = sheriff.load_config_file(open(args.procman_config_file)) 165 | except Exception as xcp: 166 | print("Unable to load config file.") 167 | print(xcp) 168 | sys.exit(1) 169 | else: 170 | cfg = None 171 | 172 | SheriffHeadless(cfg, args.spawn_deputy, args.script, args.script_done_action).run() 173 | 174 | 175 | if __name__ == "__main__": 176 | main() 177 | -------------------------------------------------------------------------------- /python/src/procman_ros/sheriff_config.py: -------------------------------------------------------------------------------- 1 | TokIdentifier = "Identifier" 2 | TokOpenStruct = "OpenStruct" 3 | TokCloseStruct = "CloseStruct" 4 | TokAssign = "Assign" 5 | TokEndStatement = "EndStatement" 6 | TokString = "String" 7 | TokEOF = "EOF" 8 | TokComment = "Comment" 9 | TokInteger = "Integer" 10 | 11 | 12 | class Token: 13 | def __init__(self, type, val): 14 | self.type = type 15 | self.val = val 16 | 17 | 18 | class ParseError(ValueError): 19 | def __init__(self, lineno, line_pos, line_text, tokenval, msg): 20 | self.lineno = lineno 21 | self.offset = line_pos 22 | self.text = line_text 23 | self.token = tokenval 24 | self.msg = msg 25 | 26 | def __str__(self): 27 | ntabs = self.text.count("\t") 28 | tokenstr = "" 29 | if self.token is not None: 30 | tokenstr = "token {}".format(self.token) 31 | s = """{} 32 | 33 | line {} col {} {} 34 | {} 35 | """.format( 36 | self.msg, self.lineno, self.offset, tokenstr, self.text 37 | ) 38 | s += " " * (self.offset - ntabs - 1) + "\t" * ntabs + "^" 39 | return s 40 | 41 | 42 | class Tokenizer: 43 | def __init__(self, f): 44 | self.f = f 45 | self.unget_char = None 46 | self.line_pos = 0 47 | self.line_len = 0 48 | self.line_buf = "" 49 | self.line_num = 1 50 | self.tok_pos = 0 51 | self.prev_tok_pos = 0 52 | 53 | def _next_char(self): 54 | if self.unget_char is not None: 55 | c = self.unget_char 56 | self.unget_char = None 57 | return c 58 | else: 59 | if self.line_pos == self.line_len: 60 | self.line_buf = self.f.readline() 61 | if not len(self.line_buf): 62 | return "" 63 | self.line_len = len(self.line_buf) 64 | self.line_pos = 0 65 | 66 | c = self.line_buf[self.line_pos] 67 | self.line_pos += 1 68 | 69 | if c == "\n": 70 | self.line_num += 1 71 | return c 72 | 73 | def _ungetc(self, c): 74 | if not c: 75 | return 76 | self.unget_char = c 77 | 78 | def _unescape(self, c): 79 | d = {"n": "\n", "r": "\r", "t": "\t"} 80 | if c in d: 81 | return d[c] 82 | return c 83 | 84 | def next_token(self): 85 | c = self._next_char() 86 | 87 | while c and c.isspace(): 88 | c = self._next_char() 89 | if not c: 90 | return Token(TokEOF, "") 91 | 92 | self.prev_tok_pos = self.tok_pos 93 | self.tok_pos = self.line_pos 94 | 95 | simple_tokens = { 96 | "=": TokAssign, 97 | ";": TokEndStatement, 98 | "{": TokOpenStruct, 99 | "}": TokCloseStruct, 100 | } 101 | if c in simple_tokens: 102 | return Token(simple_tokens[c], c) 103 | 104 | tok_chars = [c] 105 | 106 | if c == "#": 107 | while True: 108 | c = self._next_char() 109 | if not c or c == "\n": 110 | return Token(TokComment, "".join(tok_chars)) 111 | tok_chars.append(c) 112 | 113 | if c == '"': 114 | tok_chars = [] 115 | while True: 116 | c = self._next_char() 117 | if c == "\n": 118 | raise ParseError( 119 | self.line_num, 120 | self.tok_pos, 121 | self.line_buf, 122 | None, 123 | "Unterminated string constant", 124 | ) 125 | if c == "\\": 126 | c = self._unescape(self._next_char()) 127 | elif not c or c == '"': 128 | return Token(TokString, "".join(tok_chars)) 129 | tok_chars.append(c) 130 | 131 | if c.isalpha() or c == "_": 132 | while True: 133 | c = self._next_char() 134 | if not c.isalnum() and c not in "_-": 135 | self._ungetc(c) 136 | return Token(TokIdentifier, "".join(tok_chars)) 137 | tok_chars.append(c) 138 | 139 | if c.isdigit(): 140 | while True: 141 | c = self._next_char() 142 | if not c.isdigit(): 143 | self._ungetc(c) 144 | return Token(TokInteger, "".join(tok_chars)) 145 | tok_chars.append(c) 146 | 147 | raise ParseError( 148 | self.line_num, self.line_pos, self.line_buf, None, "Invalid character" 149 | ) 150 | 151 | 152 | def escape_str(text): 153 | def escape_char(c): 154 | if c in r"\"": 155 | return "\\" + c 156 | return c 157 | 158 | return "".join([escape_char(c) for c in text]) 159 | 160 | 161 | class CommandNode: 162 | def __init__(self): 163 | self.attributes = { 164 | "exec": None, 165 | "host": None, 166 | "group": "", 167 | "command_id": "", 168 | "stop_signal": 0, 169 | "stop_time_allowed": 0, 170 | } 171 | 172 | def to_config_string(self, indent=0): 173 | s = " " * indent 174 | lines = [] 175 | command_id = self.attributes["command_id"] 176 | lines.append(s + 'cmd "{}" {{'.format(escape_str(command_id))) 177 | pairs = list(self.attributes.items()) 178 | pairs.sort() 179 | for key, val in pairs: 180 | if not val: 181 | continue 182 | if key in ["group", "command_id"]: 183 | continue 184 | lines.append(s + ' {} = "{}";'.format(key, escape_str(val))) 185 | lines.append(s + "}") 186 | return "\n".join(lines) 187 | 188 | def __str__(self): 189 | return self.to_config_string() 190 | 191 | 192 | class GroupNode: 193 | def __init__(self, name): 194 | self.name = name 195 | self.commands = [] 196 | self.subgroups = {} 197 | 198 | def add_command(self, command): 199 | command.attributes["group"] = self.name 200 | self.commands.append(command) 201 | 202 | def get_subgroup(self, name_parts, create=False): 203 | if not name_parts: 204 | return self 205 | next_name = name_parts[0] 206 | if next_name in self.subgroups: 207 | return self.subgroups[next_name].get_subgroup(name_parts[1:], create) 208 | elif create: 209 | subgroup = GroupNode(next_name) 210 | self.subgroups[next_name] = subgroup 211 | return subgroup.get_subgroup(name_parts[1:], create) 212 | else: 213 | raise KeyError() 214 | 215 | def to_config_string(self, indent=0): 216 | s = " " * indent 217 | if self.name == "": 218 | assert indent == 0 219 | val = "\n".join( 220 | [group.to_config_string(0) for group in list(self.subgroups.values())] 221 | ) 222 | val = ( 223 | val 224 | + "\n".join([cmd.to_config_string(0) for cmd in self.commands]) 225 | + "\n" 226 | ) 227 | else: 228 | val = '{}group "{}" {{\n'.format(s, self.name) 229 | val = val + "\n".join( 230 | [ 231 | group.to_config_string(indent + 1) 232 | for group in list(self.subgroups.values()) 233 | ] 234 | ) 235 | val = val + "\n".join( 236 | [cmd.to_config_string(indent + 1) for cmd in self.commands] 237 | ) 238 | val = val + "\n{}}}\n".format(s) 239 | return val 240 | 241 | def __str__(self): 242 | return self.to_config_string(0) 243 | 244 | 245 | class StartStopRestartActionNode: 246 | def __init__(self, action_type, ident_type, ident, wait_status): 247 | assert action_type in ["start", "stop", "restart"] 248 | assert ident_type in ["everything", "group", "cmd"] 249 | self.action_type = action_type 250 | self.ident_type = ident_type 251 | self.wait_status = wait_status 252 | assert wait_status in [None, "running", "stopped"] 253 | if self.ident_type == "everything": 254 | self.ident = None 255 | else: 256 | self.ident = ident 257 | assert self.ident is not None 258 | 259 | def __str__(self): 260 | if self.ident_type == "everything": 261 | ident_str = self.ident_type 262 | else: 263 | ident_str = '{} "{}"'.format(self.ident_type, escape_str(self.ident)) 264 | if self.wait_status is not None: 265 | return '{} {} wait "{}";'.format( 266 | self.action_type, ident_str, self.wait_status 267 | ) 268 | else: 269 | return "{} {};".format(self.action_type, ident_str) 270 | 271 | 272 | class WaitMsActionNode: 273 | def __init__(self, delay_ms): 274 | self.delay_ms = delay_ms 275 | self.action_type = "wait_ms" 276 | 277 | def __str__(self): 278 | return "wait ms {};".format(self.delay_ms) 279 | 280 | 281 | class WaitStatusActionNode: 282 | def __init__(self, ident_type, ident, wait_status): 283 | self.ident_type = ident_type 284 | self.ident = ident 285 | self.wait_status = wait_status 286 | self.action_type = "wait_status" 287 | assert wait_status in ["running", "stopped"] 288 | 289 | def __str__(self): 290 | return 'wait {} "{}" status "{}";'.format( 291 | self.ident_type, escape_str(self.ident), self.wait_status 292 | ) 293 | 294 | 295 | class RunScriptActionNode: 296 | def __init__(self, script_name): 297 | self.script_name = script_name 298 | self.action_type = "run_script" 299 | 300 | def __str__(self): 301 | return 'run_script "{}";'.format(escape_str(self.script_name)) 302 | 303 | 304 | class ScriptNode: 305 | def __init__(self, name): 306 | self.name = name 307 | self.actions = [] 308 | 309 | def add_action(self, action): 310 | assert action is not None 311 | assert hasattr(action, "action_type") 312 | self.actions.append(action) 313 | 314 | def __str__(self): 315 | val = 'script "{}" {{'.format(escape_str(self.name)) 316 | for action in self.actions: 317 | val = val + "\n " + str(action) 318 | val = val + "\n}\n" 319 | return val 320 | 321 | 322 | class ConfigNode: 323 | def __init__(self): 324 | self.scripts = {} 325 | self.root_group = GroupNode("") 326 | 327 | def _normalize_group_name(self, name): 328 | if not name.startswith("/"): 329 | name = "/" + name 330 | while name.find("//") >= 0: 331 | name = name.replace("//", "/") 332 | return name.rstrip("/") 333 | 334 | def has_group(self, group_name): 335 | name = self._normalize_group_name(group_name) 336 | parts = group_name.split("/") 337 | group = self.root_group 338 | assert group.name == parts[0] 339 | for part in parts: 340 | if part in group.subgroups: 341 | group = group.subgroups[part] 342 | else: 343 | return False 344 | return True 345 | 346 | def get_group(self, group_name, create=False): 347 | name = self._normalize_group_name(group_name) 348 | parts = name.split("/") 349 | group = self.root_group 350 | return group.get_subgroup(parts[1:], create) 351 | 352 | def add_script(self, script): 353 | assert script.name not in self.scripts 354 | self.scripts[script.name] = script 355 | 356 | def __str__(self): 357 | val = self.root_group.to_config_string() 358 | scripts = sorted(list(self.scripts.values()), key=lambda s: s.name.lower()) 359 | val += "\n" + "\n".join([str(script) for script in scripts]) 360 | return val 361 | 362 | 363 | class Parser: 364 | def __init__(self): 365 | self.tokenizer = None 366 | self._cur_tok = None 367 | self._next_tok = None 368 | 369 | def _get_token(self): 370 | self._cur_tok = self._next_tok 371 | self._next_tok = self.tokenizer.next_token() 372 | while self._next_tok.type == TokComment: 373 | self._next_tok = self.tokenizer.next_token() 374 | return self._cur_tok 375 | 376 | def _eat_token(self, tok_type): 377 | if self._next_tok and self._next_tok.type == tok_type: 378 | self._get_token() 379 | return True 380 | return False 381 | 382 | def _fail(self, msg): 383 | raise ParseError( 384 | self.tokenizer.line_num, 385 | self.tokenizer.prev_tok_pos, 386 | self.tokenizer.line_buf, 387 | self._cur_tok.val, 388 | msg, 389 | ) 390 | 391 | def _fail_next_token(self, msg): 392 | raise ParseError( 393 | self.tokenizer.line_num, 394 | self.tokenizer.tok_pos, 395 | self.tokenizer.line_buf, 396 | self._next_tok.val, 397 | msg, 398 | ) 399 | 400 | def _eat_token_or_fail(self, tok_type, err_msg): 401 | if not self._eat_token(tok_type): 402 | self._fail_next_token(err_msg) 403 | return self._cur_tok.val 404 | 405 | def _expect_identifier(self, identifier, err_msg=None): 406 | if err_msg is None: 407 | err_msg = "Expected {}".format(identifier) 408 | self._eat_token_or_fail(TokIdentifier, err_msg) 409 | if self._cur_tok.val != identifier: 410 | self._fail(err_msg) 411 | 412 | def _parse_identifier_one_of(self, valid_identifiers): 413 | err_msg = "Expected one of {}".format(str(valid_identifiers)) 414 | self._eat_token_or_fail(TokIdentifier, err_msg) 415 | result = self._cur_tok.val 416 | if result not in valid_identifiers: 417 | self._fail(err_msg) 418 | return result 419 | 420 | def _parse_string_one_of(self, valid_strings): 421 | err_msg = "Expected one of {}".format(str(valid_strings)) 422 | self._eat_token_or_fail(TokString, err_msg) 423 | result = self._cur_tok.val 424 | if result not in valid_strings: 425 | self._fail(err_msg) 426 | return result 427 | 428 | def _parse_string_or_fail(self): 429 | self._eat_token_or_fail(TokString, "Expected string literal") 430 | return self._cur_tok.val 431 | 432 | def _parse_command_param_list(self, cmd): 433 | if not self._eat_token(TokIdentifier): 434 | return 435 | attrib_name = self._cur_tok.val 436 | 437 | attribs = { 438 | "exec": TokString, 439 | "host": TokString, 440 | "auto_respawn": TokString, 441 | "group": TokString, 442 | "stop_signal": TokInteger, 443 | "stop_time_allowed": TokInteger, 444 | } 445 | 446 | if attrib_name not in attribs: 447 | self._fail("Unrecognized attribute {}".format(attrib_name)) 448 | 449 | self._eat_token_or_fail(TokAssign, "Expected '='") 450 | if attribs[attrib_name] == TokString: 451 | attrib_val = self._parse_string_or_fail() 452 | else: 453 | self._eat_token_or_fail(TokInteger, "Expected integer literal") 454 | attrib_val = int(self._cur_tok.val) 455 | self._eat_token_or_fail(TokEndStatement, "Expected ';'") 456 | if attrib_name == "stop_signal" and attrib_val < 1: 457 | self._fail("Invalid value specified for command attribute 'stop_signal'") 458 | elif attrib_name == "stop_time_allowed" and attrib_val < 1: 459 | self._fail( 460 | "Invalid value specified for command attribute 'stop_time_allwoed'" 461 | ) 462 | cmd.attributes[attrib_name] = attrib_val 463 | 464 | return self._parse_command_param_list(cmd) 465 | 466 | def _parse_command(self): 467 | cmd = CommandNode() 468 | if self._eat_token(TokString): 469 | cmd.attributes["command_id"] = self._cur_tok.val 470 | if "/" in self._cur_tok.val: 471 | self._fail("'/' character not allowed in command id") 472 | self._eat_token_or_fail(TokOpenStruct, "Expected '{'") 473 | self._parse_command_param_list(cmd) 474 | self._eat_token_or_fail(TokCloseStruct, "Expected '}'") 475 | if not cmd.attributes["exec"]: 476 | self._fail("Invalid command defined -- no executable specified") 477 | return cmd 478 | 479 | def _parse_group(self, parent_group): 480 | self._eat_token_or_fail(TokString, "Expected group name string") 481 | if "/" in self._cur_tok.val: 482 | self._fail("'/' character is not allowed in group name") 483 | elif not self._cur_tok.val.strip(): 484 | self._fail("Empty group name is not allowed") 485 | name = self._cur_tok.val 486 | group = parent_group.get_subgroup([name], True) 487 | self._eat_token_or_fail(TokOpenStruct, "Expected '{'") 488 | while self._eat_token(TokIdentifier): 489 | if self._cur_tok.val == "cmd": 490 | group.add_command(self._parse_command()) 491 | elif self._cur_tok.val == "group": 492 | self._parse_group(group) 493 | else: 494 | self._fail("Expected one of [group, cmd]") 495 | self._eat_token_or_fail(TokCloseStruct, "Expected '}'") 496 | 497 | def _parse_start_stop_restart_action(self, action_type): 498 | valid_ident_types = ["everything", "cmd", "group"] 499 | ident_type = self._parse_identifier_one_of(valid_ident_types) 500 | ident = None 501 | if ident_type != "everything": 502 | ident = self._parse_string_or_fail() 503 | if self._eat_token(TokEndStatement): 504 | return StartStopRestartActionNode(action_type, ident_type, ident, None) 505 | self._expect_identifier("wait", "Expected ';' or 'wait'") 506 | wait_status = self._parse_string_one_of(["running", "stopped"]) 507 | self._eat_token_or_fail(TokEndStatement, "Expected ';'") 508 | return StartStopRestartActionNode(action_type, ident_type, ident, wait_status) 509 | 510 | def _parse_wait_action(self): 511 | wait_type = self._parse_identifier_one_of(["ms", "cmd", "group"]) 512 | if wait_type == "ms": 513 | err_msg = "Expected integer constant" 514 | delay_ms = int(self._eat_token_or_fail(TokInteger, err_msg)) 515 | self._eat_token_or_fail(TokEndStatement, "Expected ';'") 516 | return WaitMsActionNode(delay_ms) 517 | else: 518 | ident = self._parse_string_or_fail() 519 | self._expect_identifier("status") 520 | wait_status = self._parse_string_one_of(["running", "stopped"]) 521 | self._eat_token_or_fail(TokEndStatement, "Expected ';'") 522 | return WaitStatusActionNode(wait_type, ident, wait_status) 523 | 524 | def _parse_run_script(self): 525 | script_name = self._eat_token_or_fail(TokString, "expected script name") 526 | self._eat_token_or_fail(TokEndStatement, "Expected ';'") 527 | return RunScriptActionNode(script_name) 528 | 529 | def _parse_script_action_list(self): 530 | self._eat_token_or_fail(TokOpenStruct, "Expected '{'") 531 | actions = [] 532 | while self._eat_token(TokIdentifier): 533 | action_type = self._cur_tok.val 534 | if action_type in ["start", "stop", "restart"]: 535 | action = self._parse_start_stop_restart_action(action_type) 536 | actions.append(action) 537 | elif action_type == "wait": 538 | actions.append(self._parse_wait_action()) 539 | elif action_type == "run_script": 540 | actions.append(self._parse_run_script()) 541 | else: 542 | self._fail("Unexpected token {}".format(action_type)) 543 | self._eat_token_or_fail(TokCloseStruct, "Unexpected token") 544 | return actions 545 | 546 | def _parse_script(self): 547 | name = self._eat_token_or_fail(TokString, "expected script name") 548 | script_node = ScriptNode(name) 549 | for action in self._parse_script_action_list(): 550 | script_node.add_action(action) 551 | self._node.add_script(script_node) 552 | 553 | def _parse_listdecl(self): 554 | while True: 555 | if self._eat_token(TokEOF): 556 | return 557 | ident_type = self._parse_identifier_one_of(["cmd", "group", "script"]) 558 | if ident_type == "cmd": 559 | self._node.root_group.add_command(self._parse_command()) 560 | if ident_type == "group": 561 | self._parse_group(self._node.root_group) 562 | if ident_type == "script": 563 | self._parse_script() 564 | 565 | def parse(self, f): 566 | self.tokenizer = Tokenizer(f) 567 | self._cur_tok = None 568 | self._next_tok = None 569 | self._get_token() 570 | self._node = ConfigNode() 571 | self._parse_listdecl() 572 | return self._node 573 | 574 | 575 | if __name__ == "__main__": 576 | import sys 577 | 578 | try: 579 | fname = sys.argv[1] 580 | except IndexError: 581 | print("usage: sheriff_config.py ") 582 | sys.exit(1) 583 | 584 | config = Parser().parse(open(fname)) 585 | print(config) 586 | -------------------------------------------------------------------------------- /python/src/procman_ros/sheriff_gtk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ori-drs/procman_ros/432bfdbd2e6001775121b92ad901581ba80b09b4/python/src/procman_ros/sheriff_gtk/__init__.py -------------------------------------------------------------------------------- /python/src/procman_ros/sheriff_gtk/command_console.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import threading 4 | import functools 5 | 6 | from gi.repository import GLib 7 | from gi.repository import GObject 8 | from gi.repository import Gtk 9 | from gi.repository import Gdk 10 | from gi.repository import Pango 11 | import rospy 12 | 13 | from procman_ros.sheriff import SheriffListener 14 | from procman_ros.msg import ProcmanOutput 15 | 16 | DEFAULT_MAX_KB_PER_SECOND = 500 17 | 18 | ANSI_CODES_TO_TEXT_TAG_PROPERTIES = { 19 | "1": ("weight", Pango.Weight.BOLD), 20 | "2": ("weight", Pango.Weight.LIGHT), 21 | "4": ("underline", Pango.Underline.SINGLE), 22 | "30": ("foreground", "black"), 23 | "31": ("foreground", "red"), 24 | "32": ("foreground", "green"), 25 | "33": ("foreground", "yellow3"), 26 | "34": ("foreground", "blue"), 27 | "35": ("foreground", "magenta"), 28 | "36": ("foreground", "cyan"), 29 | "37": ("foreground", "white"), 30 | "40": ("background", "black"), 31 | "41": ("background", "red"), 32 | "42": ("background", "green"), 33 | "43": ("background", "yellow3"), 34 | "44": ("background", "blue"), 35 | "45": ("background", "magenta"), 36 | "46": ("background", "cyan"), 37 | "47": ("background", "white"), 38 | } 39 | 40 | 41 | def now_str(): 42 | return time.strftime("[%H:%M:%S] ") 43 | 44 | 45 | class CommandExtraData: 46 | def __init__(self, text_tag_table): 47 | self.tb = Gtk.TextBuffer.new(text_tag_table) 48 | self.printf_keep_count = [0, 0, 0, 0, 0, 0] 49 | self.printf_drop_count = 0 50 | 51 | 52 | class SheriffCommandConsole(Gtk.ScrolledWindow, SheriffListener): 53 | def __init__(self, _sheriff): 54 | super(SheriffCommandConsole, self).__init__() 55 | 56 | self._prev_can_reach_master = True 57 | self._ros_master_ip = os.popen( 58 | "echo $ROS_MASTER_URI").read().split("//")[1].split(":")[0] 59 | 60 | self.stdout_maxlines = 250 61 | self.max_kb_per_sec = 0 62 | self.max_chars_per_2500_ms = 0 63 | 64 | self.sheriff = _sheriff 65 | 66 | # stdout textview 67 | self.stdout_textview = Gtk.TextView() 68 | self.stdout_textview.set_property("editable", False) 69 | self.sheriff_tb = self.stdout_textview.get_buffer() 70 | self.add(self.stdout_textview) 71 | 72 | stdout_adj = self.get_vadjustment() 73 | stdout_adj.scrolled_to_end = 1 74 | stdout_adj.connect("changed", self.on_adj_changed) 75 | stdout_adj.connect("value-changed", self.on_adj_value_changed) 76 | 77 | # deal with keyboard shortcuts 78 | self.connect("key-release-event", self.on_key_release) 79 | 80 | # add callback so we can add a clear option to the default right click popup 81 | self.stdout_textview.connect( 82 | "populate-popup", self.on_tb_populate_menu) 83 | 84 | # set some default appearance parameters 85 | self.font_str = "Monospace 10" 86 | self.set_font(self.font_str) 87 | self.base_color = Gdk.Color(65535, 65535, 65535) 88 | self.text_color = Gdk.Color(0, 0, 0) 89 | self.set_background_color(self.base_color) 90 | self.set_text_color(self.text_color) 91 | 92 | # stdout rate limit maintenance events 93 | GObject.timeout_add(500, self._stdout_rate_limit_upkeep) 94 | 95 | self.sheriff.add_listener(self) 96 | 97 | self._cmd_extradata = {} 98 | 99 | self.output_sub = rospy.Subscriber( 100 | "/procman/output", ProcmanOutput, self.on_procman_output, queue_size=10 101 | ) 102 | 103 | self.text_tags = {"normal": Gtk.TextTag.new("normal")} 104 | for tt in list(self.text_tags.values()): 105 | self.sheriff_tb.get_tag_table().add(tt) 106 | 107 | self.set_output_rate_limit(DEFAULT_MAX_KB_PER_SECOND) 108 | 109 | self._master_reach_check_thread = threading.Thread( 110 | target=self._master_reach_check) 111 | self._master_reach_check_thread.start() 112 | 113 | def command_added(self, deputy_obj, cmd_obj): 114 | GLib.idle_add(self._gtk_on_sheriff_command_added, deputy_obj, cmd_obj) 115 | 116 | def command_removed(self, deputy_obj, cmd_obj): 117 | GLib.idle_add(self._gtk_on_sheriff_command_removed, 118 | deputy_obj, cmd_obj) 119 | 120 | def command_status_changed(self, cmd_obj, old_status, new_status): 121 | GLib.idle_add( 122 | self._gtk_on_command_desired_changed, cmd_obj, old_status, new_status 123 | ) 124 | 125 | def get_background_color(self): 126 | return self.base_color 127 | 128 | def get_text_color(self): 129 | return self.text_color 130 | 131 | def get_font(self): 132 | return self.font_str 133 | 134 | def set_background_color(self, color): 135 | self.base_color = color 136 | self.stdout_textview.modify_base(Gtk.StateType.NORMAL, color) 137 | self.stdout_textview.modify_base(Gtk.StateType.ACTIVE, color) 138 | self.stdout_textview.modify_base(Gtk.StateType.PRELIGHT, color) 139 | 140 | def set_text_color(self, color): 141 | self.text_color = color 142 | self.stdout_textview.modify_text(Gtk.StateType.NORMAL, color) 143 | self.stdout_textview.modify_text(Gtk.StateType.ACTIVE, color) 144 | self.stdout_textview.modify_text(Gtk.StateType.PRELIGHT, color) 145 | 146 | def set_font(self, font_str): 147 | self.font_str = font_str 148 | self.stdout_textview.modify_font(Pango.FontDescription(font_str)) 149 | 150 | def _master_reach_check(self): 151 | 152 | while not self.sheriff._exiting: 153 | curr_can_reach_master = False 154 | 155 | response = os.system( 156 | "ping -c 1 -w 1 {} >/dev/null 2>&1".format(self._ros_master_ip)) 157 | if response == 0: 158 | curr_can_reach_master = True 159 | 160 | # print('Prev can reach: {}'.format(self._prev_can_reach_master)) 161 | # print('Curr can reach: {}'.format(curr_can_reach_master)) 162 | if not self._prev_can_reach_master and curr_can_reach_master: 163 | self.output_sub.unregister() 164 | self.output_sub = rospy.Subscriber( 165 | "/procman/output", ProcmanOutput, self.on_procman_output, queue_size=10 166 | ) 167 | self._prev_can_reach_master = curr_can_reach_master 168 | time.sleep(5) 169 | 170 | def _stdout_rate_limit_upkeep(self): 171 | for cmd in self.sheriff.get_all_commands(): 172 | extradata = self._cmd_extradata.get(cmd, None) 173 | if not extradata: 174 | continue 175 | if extradata.printf_drop_count: 176 | deputy = self.sheriff.get_command_deputy(cmd) 177 | self._add_text_to_buffer( 178 | extradata.tb, 179 | now_str() 180 | + "\nSHERIFF RATE LIMIT: Ignored {} bytes of output\n".format( 181 | extradata.printf_drop_count 182 | ), 183 | ) 184 | self._add_text_to_buffer( 185 | self.sheriff_tb, 186 | now_str() 187 | + "Ignored {} bytes of output from [{}] [{}]\n".format( 188 | extradata.printf_drop_count, deputy.deputy_id, cmd.command_id 189 | ), 190 | ) 191 | 192 | extradata.printf_keep_count.pop(0) 193 | extradata.printf_keep_count.append(0) 194 | extradata.printf_drop_count = 0 195 | return True 196 | 197 | def _tag_from_seg(self, seg): 198 | esc_seq, seg = seg.split("m", 1) 199 | if not esc_seq: 200 | esc_seq = "0" 201 | key = esc_seq 202 | codes = esc_seq.split(";") 203 | if len(codes) > 0: 204 | codes.sort() 205 | key = ";".join(codes) 206 | if key not in self.text_tags: 207 | tag = Gtk.TextTag.new(key) 208 | for code in codes: 209 | if code in ANSI_CODES_TO_TEXT_TAG_PROPERTIES: 210 | propname, propval = ANSI_CODES_TO_TEXT_TAG_PROPERTIES[code] 211 | tag.set_property(propname, propval) 212 | self.sheriff_tb.get_tag_table().add(tag) 213 | self.text_tags[key] = tag 214 | return self.text_tags[key], seg 215 | 216 | def _add_text_to_buffer(self, tb, text): 217 | if not text: 218 | return 219 | 220 | # interpret text as ANSI escape sequences? Try to format colors... 221 | tag = self.text_tags["normal"] 222 | for segnum, seg in enumerate(text.split("\x1b[")): 223 | if not seg: 224 | continue 225 | if segnum > 0: 226 | try: 227 | tag, seg = self._tag_from_seg(seg) 228 | except ValueError: 229 | pass 230 | end_iter = tb.get_end_iter() 231 | tb.insert_with_tags(end_iter, seg, tag) 232 | 233 | # toss out old text if the buffer is getting too big 234 | num_lines = tb.get_line_count() 235 | if num_lines > self.stdout_maxlines: 236 | start_iter = tb.get_start_iter() 237 | chop_iter = tb.get_iter_at_line(num_lines - self.stdout_maxlines) 238 | # Must use idle_add here otherwise the output console will not be updated correctly 239 | GLib.idle_add(functools.partial( 240 | self.del_tb, start_iter, chop_iter)) 241 | 242 | def del_tb(self, start, chop): 243 | self.sheriff_tb.delete(start, chop) 244 | 245 | # Sheriff event handlers 246 | def _gtk_on_sheriff_command_added(self, deputy, command): 247 | extradata = CommandExtraData(self.sheriff_tb.get_tag_table()) 248 | self._cmd_extradata[command] = extradata 249 | self._add_text_to_buffer( 250 | self.sheriff_tb, 251 | now_str() 252 | + "Added [{}] [{}] [{}]\n".format( 253 | deputy.deputy_id, command.command_id, command.exec_str 254 | ), 255 | ) 256 | 257 | def _gtk_on_sheriff_command_removed(self, deputy, command): 258 | if command in self._cmd_extradata: 259 | del self._cmd_extradata[command] 260 | self._add_text_to_buffer( 261 | self.sheriff_tb, 262 | now_str() 263 | + "[{}] removed [{}] [{}]\n".format( 264 | deputy.deputy_id, command.command_id, command.exec_str 265 | ), 266 | ) 267 | 268 | def _gtk_on_command_desired_changed(self, cmd, old_status, new_status): 269 | self._add_text_to_buffer( 270 | self.sheriff_tb, 271 | now_str() + 272 | "[{}] new status: {}\n".format(cmd.command_id, new_status), 273 | ) 274 | 275 | def on_tb_populate_menu(self, textview, menu): 276 | sep = Gtk.SeparatorMenuItem() 277 | menu.append(sep) 278 | sep.show() 279 | mi = Gtk.MenuItem("_Clear") 280 | menu.append(mi) 281 | mi.connect("activate", self._tb_clear) 282 | mi.show() 283 | 284 | def _tb_clear(self, menu): 285 | tb = self.stdout_textview.get_buffer() 286 | start_iter = tb.get_start_iter() 287 | end_iter = tb.get_end_iter() 288 | tb.delete(start_iter, end_iter) 289 | 290 | def _tb_copy_selection(self): 291 | tb = self.stdout_textview.get_buffer() 292 | bounds = tb.get_selection_bounds() 293 | 294 | if bounds: 295 | text = tb.get_text(bounds[0], bounds[1], True) 296 | clipboard = Gtk.Clipboard() 297 | clipboard.set_text(text) 298 | clipboard.store() 299 | 300 | def on_key_release(self, widget, event): 301 | key_value = event.keyval 302 | key_name = Gdk.keyval_name(key_value) 303 | state = event.get_state() 304 | ctrl = state & Gdk.ModifierType.CONTROL_MASK 305 | if ctrl and key_name == "c": 306 | self._tb_copy_selection() 307 | return True 308 | else: 309 | return False 310 | 311 | def set_output_rate_limit(self, max_kb_per_sec): 312 | self.max_kb_per_sec = max_kb_per_sec 313 | self.max_chars_per_2500_ms = int(max_kb_per_sec * 1000 * 2.5) 314 | 315 | def get_output_rate_limit(self): 316 | return self.max_kb_per_sec 317 | 318 | def load_settings(self, save_map): 319 | if "console_rate_limit" in save_map: 320 | self.set_output_rate_limit(save_map["console_rate_limit"]) 321 | 322 | if "console_background_color" in save_map: 323 | bg_color = Gdk.RGBA() 324 | bg_color.parse(save_map["console_background_color"]) 325 | self.set_background_color(bg_color.to_color()) 326 | 327 | if "console_text_color" in save_map: 328 | text_color = Gdk.RGBA() 329 | text_color.parse(save_map["console_text_color"]) 330 | self.set_text_color(text_color.to_color()) 331 | 332 | if "console_font" in save_map: 333 | self.set_font(save_map["console_font"]) 334 | 335 | def save_settings(self, save_map): 336 | save_map["console_rate_limit"] = self.max_kb_per_sec 337 | save_map["console_background_color"] = self.base_color.to_string() 338 | save_map["console_text_color"] = self.text_color.to_string() 339 | save_map["console_font"] = self.font_str 340 | 341 | def on_adj_changed(self, adj): 342 | if adj.scrolled_to_end: 343 | adj.set_value(adj.get_upper() - adj.get_page_size()) 344 | 345 | def on_adj_value_changed(self, adj): 346 | adj.scrolled_to_end = adj.get_value() == ( 347 | adj.get_upper() - adj.get_page_size()) 348 | 349 | def _handle_command_output(self, command_id, text): 350 | cmd = self.sheriff.get_command(command_id) 351 | if not cmd: 352 | return 353 | 354 | extradata = self._cmd_extradata.get(cmd, None) 355 | if not extradata: 356 | return 357 | 358 | # rate limit 359 | msg_count = sum(extradata.printf_keep_count) 360 | if msg_count >= self.max_chars_per_2500_ms: 361 | extradata.printf_drop_count += len(text) 362 | return 363 | 364 | tokeep = min(self.max_chars_per_2500_ms - msg_count, len(text)) 365 | extradata.printf_keep_count[-1] += tokeep 366 | 367 | if len(text) > tokeep: 368 | toadd = text[:tokeep] 369 | else: 370 | toadd = text 371 | 372 | self._add_text_to_buffer(extradata.tb, toadd) 373 | 374 | def on_procman_output(self, msg): 375 | for i in range(msg.num_commands): 376 | command_id = msg.command_ids[i] 377 | text = msg.text[i] 378 | GLib.idle_add(self._handle_command_output, command_id, text) 379 | 380 | def show_command_buffer(self, cmd): 381 | extradata = self._cmd_extradata.get(cmd, None) 382 | if extradata: 383 | self.stdout_textview.set_buffer(extradata.tb) 384 | 385 | def show_sheriff_buffer(self): 386 | self.stdout_textview.set_buffer(self.sheriff_tb) 387 | -------------------------------------------------------------------------------- /python/src/procman_ros/sheriff_gtk/command_model.py: -------------------------------------------------------------------------------- 1 | from gi.repository import GObject 2 | from gi.repository import Gtk 3 | 4 | import procman_ros.sheriff as sheriff 5 | 6 | ( 7 | COL_CMDS_TV_OBJ, 8 | COL_CMDS_TV_EXEC, 9 | COL_CMDS_TV_FULL_GROUP, 10 | COL_CMDS_TV_COMMAND_ID, 11 | COL_CMDS_TV_DEPUTY, 12 | COL_CMDS_TV_STATUS_ACTUAL, 13 | COL_CMDS_TV_CPU_USAGE, 14 | COL_CMDS_TV_MEM_RSS, 15 | COL_CMDS_TV_AUTO_RESPAWN, 16 | NUM_CMDS_ROWS, 17 | ) = list(range(10)) 18 | 19 | 20 | class SheriffCommandModel(Gtk.TreeStore): 21 | def __init__(self, _sheriff): 22 | super(SheriffCommandModel, self).__init__( 23 | GObject.TYPE_PYOBJECT, 24 | GObject.TYPE_STRING, # command executable 25 | GObject.TYPE_STRING, # group name 26 | GObject.TYPE_STRING, # display name 27 | GObject.TYPE_STRING, # deputy id 28 | GObject.TYPE_STRING, # status actual 29 | GObject.TYPE_STRING, # CPU usage 30 | GObject.TYPE_INT, # memory vsize 31 | GObject.TYPE_BOOLEAN, # auto-respawn 32 | ) 33 | 34 | self.sheriff = _sheriff 35 | self.group_row_references = {} 36 | self.populate_exec_with_group_name = False 37 | 38 | self.set_sort_column_id(COL_CMDS_TV_COMMAND_ID, Gtk.SortType.ASCENDING) 39 | 40 | def _find_or_make_group_row_reference(self, group_name): 41 | if not group_name: 42 | return None 43 | if group_name in self.group_row_references: 44 | return self.group_row_references[group_name] 45 | else: 46 | name_parts = group_name.split("/") 47 | if len(name_parts) > 1: 48 | parent_name = "/".join(name_parts[:-1]) 49 | parent_row_ref = self._find_or_make_group_row_reference(parent_name) 50 | parent = self.get_iter(parent_row_ref.get_path()) 51 | else: 52 | parent = None 53 | 54 | if self.populate_exec_with_group_name: 55 | exec_val = name_parts[-1] 56 | else: 57 | exec_val = "" 58 | 59 | new_row = ( 60 | None, # COL_CMDS_TV_OBJ 61 | exec_val, # COL_CMDS_TV_EXEC 62 | group_name, # COL_CMDS_TV_FULL_GROUP 63 | name_parts[-1], # COL_CMDS_TV_COMMAND_ID 64 | "", # COL_CMDS_TV_DEPUTY 65 | "", # COL_CMDS_TV_STATUS_ACTUAL 66 | "", # COL_CMDS_TV_CPU_USAGE 67 | 0, # COL_CMDS_TV_MEM_RSS 68 | False, # COL_CMDS_TV_AUTO_RESPAWN 69 | ) 70 | ts_iter = self.append(parent, new_row) 71 | trr = Gtk.TreeRowReference(self, self.get_path(ts_iter)) 72 | self.group_row_references[group_name] = trr 73 | return trr 74 | 75 | def get_known_group_names(self): 76 | return list(self.group_row_references.keys()) 77 | 78 | def set_populate_exec_with_group_name(self, val): 79 | self.populate_exec_with_group_name = val 80 | 81 | def _delete_group_row_reference(self, trr): 82 | model_iter = self.get_iter(trr.get_path()) 83 | group_name = self.get_value(model_iter, COL_CMDS_TV_FULL_GROUP) 84 | del self.group_row_references[group_name] 85 | self.remove(model_iter) 86 | 87 | def _is_group_row(self, model_iter): 88 | return self.iter_to_command(model_iter) is None 89 | 90 | def _update_cmd_row(self, model_rr, cmd_deps, to_reparent): 91 | path = model_rr.get_path() 92 | model_iter = self.get_iter(path) 93 | cmd = self.iter_to_command(model_iter) 94 | cpu_str = "{:.2f}".format(cmd.cpu_usage * 100) 95 | mem_usage = int(cmd.mem_rss_bytes / 1024) 96 | 97 | self.set( 98 | model_iter, 99 | COL_CMDS_TV_EXEC, 100 | cmd.exec_str, 101 | COL_CMDS_TV_COMMAND_ID, 102 | cmd.command_id, 103 | COL_CMDS_TV_STATUS_ACTUAL, 104 | cmd.status(), 105 | COL_CMDS_TV_DEPUTY, 106 | cmd_deps[cmd].deputy_id, 107 | COL_CMDS_TV_CPU_USAGE, 108 | cpu_str, 109 | COL_CMDS_TV_MEM_RSS, 110 | mem_usage, 111 | COL_CMDS_TV_AUTO_RESPAWN, 112 | cmd.auto_respawn, 113 | ) 114 | 115 | # get a row reference to the model since 116 | # adding a group may invalidate the iterators 117 | model_rr = Gtk.TreeRowReference(self, path) 118 | 119 | # check that the command is in the correct group in the 120 | # treemodel 121 | correct_grr = self._find_or_make_group_row_reference(cmd.group) 122 | correct_parent_iter = None 123 | correct_parent_path = None 124 | actual_parent_path = None 125 | if correct_grr and correct_grr.get_path() is not None: 126 | correct_parent_iter = self.get_iter(correct_grr.get_path()) 127 | actual_parent_iter = self.iter_parent( 128 | self.get_iter(model_rr.get_path()) 129 | ) # use the model_rr in case model_iter was invalidated 130 | 131 | if correct_parent_iter: 132 | correct_parent_path = self.get_path(correct_parent_iter) 133 | if actual_parent_iter: 134 | actual_parent_path = self.get_path(actual_parent_iter) 135 | 136 | if correct_parent_path != actual_parent_path: 137 | # schedule the command to be moved 138 | to_reparent.append((model_rr, correct_grr)) 139 | 140 | # print "moving %s (%s) (%s)" % (cmd.name, 141 | # correct_parent_path, actual_parent_path) 142 | 143 | def _update_group_row(self, group_rr, cmd_deps): 144 | model_iter = self.get_iter(group_rr.get_path()) 145 | # row represents a procman_ros group 146 | children = self.get_group_row_child_commands_recursive(model_iter) 147 | if not children: 148 | return 149 | 150 | # aggregate command status 151 | statuses = [cmd.status() for cmd in children] 152 | stopped_statuses = [sheriff.STOPPED_OK, sheriff.STOPPED_ERROR] 153 | if all([s == statuses[0] for s in statuses]): 154 | status_str = statuses[0] 155 | elif all([s in stopped_statuses for s in statuses]): 156 | status_str = "Stopped (Mixed)" 157 | else: 158 | status_str = "Mixed" 159 | 160 | # aggregate deputy information 161 | child_deps = {cmd_deps[child] for child in children if child in cmd_deps} 162 | if len(child_deps) == 1: 163 | dep_str = child_deps.pop().deputy_id 164 | else: 165 | dep_str = "Mixed" 166 | 167 | # aggregate CPU and memory usage 168 | cpu_total = sum([cmd.cpu_usage for cmd in children]) 169 | mem_total = sum([cmd.mem_rss_bytes / 1024 for cmd in children]) 170 | cpu_str = "{:.2f}".format(cpu_total * 100) 171 | 172 | # display group name in command column? 173 | if self.populate_exec_with_group_name: 174 | exec_val = self.get_value(model_iter, COL_CMDS_TV_COMMAND_ID) 175 | else: 176 | exec_val = "" 177 | 178 | self.set( 179 | model_iter, 180 | COL_CMDS_TV_STATUS_ACTUAL, 181 | status_str, 182 | COL_CMDS_TV_EXEC, 183 | exec_val, 184 | COL_CMDS_TV_DEPUTY, 185 | dep_str, 186 | COL_CMDS_TV_CPU_USAGE, 187 | cpu_str, 188 | COL_CMDS_TV_MEM_RSS, 189 | mem_total, 190 | ) 191 | 192 | def _dispatch_row_changes(self, model, path, model_iter, user_data): 193 | ( 194 | cmds_to_add, 195 | cmd_rows_to_remove, 196 | cmds_rows_to_update, 197 | group_rows_to_update, 198 | ) = user_data 199 | cmd = self.iter_to_command(model_iter) 200 | if cmd: 201 | if cmd in cmds_to_add: 202 | cmds_rows_to_update.append(Gtk.TreeRowReference(model, path)) 203 | cmds_to_add.remove(cmd) 204 | else: 205 | cmd_rows_to_remove.append(Gtk.TreeRowReference(model, path)) 206 | else: 207 | group_rows_to_update.append(Gtk.TreeRowReference(model, path)) 208 | 209 | def repopulate(self): 210 | cmds_to_add = set() 211 | cmd_deps = {} 212 | for deputy in self.sheriff.get_deputies(): 213 | for cmd in deputy.get_commands(): 214 | cmd_deps[cmd] = deputy 215 | cmds_to_add.add(cmd) 216 | cmd_rows_to_remove = [] 217 | cmd_rows_to_reparent = [] 218 | cmds_rows_to_update = [] 219 | group_rows_to_update = [] 220 | 221 | # Figure out which rows should be added/updated/removed etc... 222 | # On return, the cmds_to_add set will 223 | # contain commands that were not updated (i.e., commands that need to 224 | # be added into the model) 225 | self.foreach( 226 | self._dispatch_row_changes, 227 | ( 228 | cmds_to_add, 229 | cmd_rows_to_remove, 230 | cmds_rows_to_update, 231 | group_rows_to_update, 232 | ), 233 | ) 234 | 235 | # update the command rows that should be updated 236 | for trr in cmds_rows_to_update: 237 | self._update_cmd_row(trr, cmd_deps, cmd_rows_to_reparent) 238 | 239 | # update the group rows that should be updated 240 | for trr in group_rows_to_update: 241 | self._update_group_row(trr, cmd_deps) 242 | 243 | # reparent rows that are in the wrong group 244 | for trr, newparent_rr in cmd_rows_to_reparent: 245 | orig_iter = self.get_iter(trr.get_path()) 246 | rowdata = self.get(orig_iter, *list(range(NUM_CMDS_ROWS))) 247 | self.remove(orig_iter) 248 | 249 | newparent_iter = None 250 | if newparent_rr: 251 | newparent_iter = self.get_iter(newparent_rr.get_path()) 252 | self.append(newparent_iter, rowdata) 253 | 254 | # remove rows that have been marked for deletion 255 | for trr in cmd_rows_to_remove: 256 | self.remove(self.get_iter(trr.get_path())) 257 | 258 | # remove group rows with no children 259 | groups_to_remove = [] 260 | 261 | def _check_for_lonely_groups(model, path, model_iter, user_data): 262 | is_group = self._is_group_row(model_iter) 263 | if is_group and not model.iter_has_child(model_iter): 264 | groups_to_remove.append(Gtk.TreeRowReference(model, path)) 265 | 266 | self.foreach(_check_for_lonely_groups, None) 267 | for trr in groups_to_remove: 268 | self._delete_group_row_reference(trr) 269 | 270 | # create new rows for new commands 271 | for cmd in cmds_to_add: 272 | deputy = cmd_deps[cmd] 273 | parent = self._find_or_make_group_row_reference(cmd.group) 274 | 275 | new_row = ( 276 | cmd, # COL_CMDS_TV_OBJ 277 | cmd.exec_str, # COL_CMDS_TV_EXEC 278 | "", # COL_CMDS_TV_FULL_GROUP 279 | cmd.command_id, # COL_CMDS_TV_COMMAND_ID 280 | deputy.deputy_id, # COL_CMDS_TV_DEPUTY 281 | cmd.status(), # COL_CMDS_TV_STATUS_ACTUAL 282 | "0", # COL_CMDS_TV_CPU_USAGE 283 | 0, # COL_CMDS_TV_MEM_RSS 284 | cmd.auto_respawn, # COL_CMDS_TV_AUTO_RESPAWN 285 | ) 286 | if parent: 287 | self.append(self.get_iter(parent.get_path()), new_row) 288 | else: 289 | self.append(None, new_row) 290 | 291 | def rows_to_commands(self, rows): 292 | col = COL_CMDS_TV_OBJ 293 | selected = set() 294 | for path in rows: 295 | cmds_iter = self.get_iter(path) 296 | cmd = self.get_value(cmds_iter, col) 297 | if cmd: 298 | selected.add(cmd) 299 | else: 300 | for child in self.get_group_row_child_commands_recursive(cmds_iter): 301 | selected.add(child) 302 | return selected 303 | 304 | def iter_to_command(self, model_iter): 305 | return self.get_value(model_iter, COL_CMDS_TV_OBJ) 306 | 307 | def path_to_command(self, path): 308 | return self.iter_to_command(self.get_iter(path)) 309 | 310 | def get_group_row_child_commands_recursive(self, group_iter): 311 | child_iter = self.iter_children(group_iter) 312 | children = [] 313 | while child_iter: 314 | child_cmd = self.iter_to_command(child_iter) 315 | if child_cmd: 316 | children.append(child_cmd) 317 | else: 318 | children += self.get_group_row_child_commands_recursive(child_iter) 319 | child_iter = self.iter_next(child_iter) 320 | return children 321 | -------------------------------------------------------------------------------- /python/src/procman_ros/sheriff_gtk/command_treeview.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | from gi.repository import Gdk 3 | from gi.repository import Pango 4 | 5 | import procman_ros.sheriff as sheriff 6 | import procman_ros.sheriff_gtk.command_model as cm 7 | import procman_ros.sheriff_gtk.sheriff_dialogs as sd 8 | 9 | 10 | class SheriffCommandTreeView(Gtk.TreeView): 11 | def __init__(self, _sheriff, _script_manager, cmds_ts): 12 | super(SheriffCommandTreeView, self).__init__(cmds_ts) 13 | self.cmds_ts = cmds_ts 14 | self.sheriff = _sheriff 15 | self.script_manager = _script_manager 16 | 17 | cmds_tr = Gtk.CellRendererText() 18 | cmds_tr.set_property("ellipsize", Pango.EllipsizeMode.END) 19 | plain_tr = Gtk.CellRendererText() 20 | status_tr = Gtk.CellRendererText() 21 | 22 | cols_to_make = [ 23 | ("Id", cmds_tr, cm.COL_CMDS_TV_COMMAND_ID, None), 24 | ("Command", cmds_tr, cm.COL_CMDS_TV_EXEC, None), 25 | ("Deputy", plain_tr, cm.COL_CMDS_TV_DEPUTY, None), 26 | ( 27 | "Status", 28 | status_tr, 29 | cm.COL_CMDS_TV_STATUS_ACTUAL, 30 | self._status_cell_data_func, 31 | ), 32 | ("CPU %", plain_tr, cm.COL_CMDS_TV_CPU_USAGE, None), 33 | ("Mem (kB)", plain_tr, cm.COL_CMDS_TV_MEM_RSS, None), 34 | ] 35 | 36 | self.columns = [] 37 | for command_id, renderer, col_id, cell_data_func in cols_to_make: 38 | col = Gtk.TreeViewColumn(command_id, renderer, text=col_id) 39 | col.set_sort_column_id(col_id) 40 | col.col_id = col_id 41 | if cell_data_func: 42 | col.set_cell_data_func(renderer, cell_data_func) 43 | self.columns.append(col) 44 | 45 | # set an initial width for the id column 46 | self.columns[0].set_sizing(Gtk.TreeViewColumnSizing.FIXED) 47 | self.columns[0].set_fixed_width(150) 48 | 49 | for col in self.columns: 50 | col.set_resizable(True) 51 | self.append_column(col) 52 | 53 | cmds_sel = self.get_selection() 54 | cmds_sel.set_mode(Gtk.SelectionMode.MULTIPLE) 55 | 56 | self.add_events( 57 | Gdk.EventMask.KEY_PRESS_MASK 58 | | Gdk.EventType.BUTTON_PRESS 59 | | Gdk.EventType._2BUTTON_PRESS 60 | ) 61 | self.connect("key-press-event", self._on_cmds_tv_key_press_event) 62 | self.connect("button-press-event", self._on_cmds_tv_button_press_event) 63 | self.connect("row-activated", self._on_cmds_tv_row_activated) 64 | 65 | # commands treeview context menu 66 | self.cmd_ctxt_menu = Gtk.Menu() 67 | 68 | self.start_cmd_ctxt_mi = Gtk.MenuItem.new_with_mnemonic("_Start") 69 | self.cmd_ctxt_menu.append(self.start_cmd_ctxt_mi) 70 | self.start_cmd_ctxt_mi.connect("activate", self._start_selected_commands) 71 | 72 | self.stop_cmd_ctxt_mi = Gtk.MenuItem.new_with_mnemonic("S_top") 73 | self.cmd_ctxt_menu.append(self.stop_cmd_ctxt_mi) 74 | self.stop_cmd_ctxt_mi.connect("activate", self._stop_selected_commands) 75 | 76 | self.restart_cmd_ctxt_mi = Gtk.MenuItem.new_with_mnemonic("R_estart") 77 | self.cmd_ctxt_menu.append(self.restart_cmd_ctxt_mi) 78 | self.restart_cmd_ctxt_mi.connect("activate", self._restart_selected_commands) 79 | 80 | self.remove_cmd_ctxt_mi = Gtk.MenuItem.new_with_mnemonic("_Remove") 81 | self.cmd_ctxt_menu.append(self.remove_cmd_ctxt_mi) 82 | self.remove_cmd_ctxt_mi.connect("activate", self._remove_selected_commands) 83 | 84 | self.cmd_ctxt_menu.append(Gtk.SeparatorMenuItem()) 85 | 86 | self.edit_cmd_ctxt_mi = Gtk.MenuItem.new_with_mnemonic("_Edit") 87 | self.cmd_ctxt_menu.append(self.edit_cmd_ctxt_mi) 88 | self.edit_cmd_ctxt_mi.connect("activate", self._edit_selected_command) 89 | 90 | self.new_cmd_ctxt_mi = Gtk.MenuItem.new_with_mnemonic("_New Command") 91 | self.cmd_ctxt_menu.append(self.new_cmd_ctxt_mi) 92 | self.new_cmd_ctxt_mi.connect( 93 | "activate", 94 | lambda *s: sd.do_add_command_dialog( 95 | self.sheriff, self.cmds_ts, self.get_toplevel() 96 | ), 97 | ) 98 | 99 | self.cmd_ctxt_menu.show_all() 100 | 101 | # # set some default appearance parameters 102 | # self.base_color = Gdk.Color(65535, 65535, 65535) 103 | # self.text_color = Gdk.Color(0, 0, 0) 104 | # self.set_background_color(self.base_color) 105 | # self.set_text_color(self.text_color) 106 | 107 | # # drag and drop command rows for grouping 108 | # dnd_targets = [ ('PROCMAN_CMD_ROW', 109 | # Gtk.TargetFlags.SAME_APP | Gtk.TargetFlags.SAME_WIDGET, 0) ] 110 | # self.enable_model_drag_source (Gdk.ModifierType.BUTTON1_MASK, 111 | # dnd_targets, Gdk.DragAction.MOVE) 112 | # self.enable_model_drag_dest (dnd_targets, 113 | # Gdk.DragAction.MOVE) 114 | 115 | def get_columns(self): 116 | return self.columns 117 | 118 | def get_selected_commands(self): 119 | selection = self.get_selection() 120 | if selection is None: 121 | return [] 122 | model, rows = selection.get_selected_rows() 123 | assert model is self.cmds_ts 124 | return self.cmds_ts.rows_to_commands(rows) 125 | 126 | # def get_background_color(self): 127 | # return self.base_color 128 | # 129 | # def get_text_color(self): 130 | # return self.text_color 131 | # 132 | # def set_background_color(self, color): 133 | # self.base_color = color 134 | # self.modify_base(Gtk.StateType.NORMAL, color) 135 | # self.modify_base(Gtk.StateType.ACTIVE, color) 136 | # self.modify_base(Gtk.StateType.PRELIGHT, color) 137 | # 138 | # def set_text_color(self, color): 139 | # self.text_color = color 140 | # self.modify_text(Gtk.StateType.NORMAL, color) 141 | # self.modify_text(Gtk.StateType.ACTIVE, color) 142 | # self.modify_text(Gtk.StateType.PRELIGHT, color) 143 | 144 | def save_settings(self, save_map): 145 | for col in self.get_columns(): 146 | col_id = col.col_id 147 | 148 | visible_key = "command_treeview:visible:{}".format(col_id) 149 | width_key = "command_treeview:width:{}".format(col_id) 150 | save_map[visible_key] = col.get_visible() 151 | save_map[width_key] = col.get_width() 152 | 153 | # save_map["command_treeview_background_color"] = self.base_color.to_string() 154 | # save_map["command_treeview_text_color"] = self.text_color.to_string() 155 | 156 | def load_settings(self, save_map): 157 | for col in self.get_columns(): 158 | col_id = col.col_id 159 | 160 | visible_key = "command_treeview:visible:{}".format(col_id) 161 | width_key = "command_treeview:width:{}".format(col_id) 162 | should_be_visible = save_map.get(visible_key, True) 163 | col.set_visible(should_be_visible) 164 | if int(col_id) == cm.COL_CMDS_TV_COMMAND_ID: 165 | self.cmds_ts.set_populate_exec_with_group_name(not should_be_visible) 166 | 167 | width = save_map.get(width_key, 0) 168 | if width > 0: 169 | col.set_sizing(Gtk.TreeViewColumnSizing.FIXED) 170 | col.set_fixed_width(width) 171 | col.set_resizable(True) 172 | 173 | # if "command_treeview_background_color" in save_map: 174 | # self.set_background_color(Gdk.Color(save_map["command_treeview_background_color"])) 175 | # 176 | # if "command_treeview_text_color" in save_map: 177 | # self.set_text_color(Gdk.Color(save_map["command_treeview_text_color"])) 178 | 179 | def _start_selected_commands(self, *args): 180 | for cmd in self.get_selected_commands(): 181 | self.sheriff.start_command(cmd) 182 | 183 | def _stop_selected_commands(self, *args): 184 | for cmd in self.get_selected_commands(): 185 | self.sheriff.stop_command(cmd) 186 | 187 | def _restart_selected_commands(self, *args): 188 | for cmd in self.get_selected_commands(): 189 | self.sheriff.restart_command(cmd) 190 | 191 | def _remove_selected_commands(self, *args): 192 | for cmd in self.get_selected_commands(): 193 | self.sheriff.remove_command(cmd) 194 | 195 | def _edit_selected_command(self, *args): 196 | cmds = self.get_selected_commands() 197 | self._do_edit_command_dialog(cmds) 198 | 199 | def _on_cmds_tv_key_press_event(self, widget, event): 200 | if event.keyval == Gdk.keyval_from_name("Right"): 201 | # expand a group row when user presses right arrow key 202 | model, rows = self.get_selection().get_selected_rows() 203 | if len(rows) == 1: 204 | model_iter = model.get_iter(rows[0]) 205 | if model.iter_has_child(model_iter): 206 | self.expand_row(rows[0], True) 207 | return True 208 | elif event.keyval == Gdk.keyval_from_name("Left"): 209 | # collapse a group row when user presses left arrow key 210 | model, rows = self.get_selection().get_selected_rows() 211 | if len(rows) == 1: 212 | model_iter = model.get_iter(rows[0]) 213 | if model.iter_has_child(model_iter): 214 | self.collapse_row(rows[0]) 215 | else: 216 | parent = model.iter_parent(model_iter) 217 | if parent: 218 | parent_path = self.cmds_ts.get_path(parent) 219 | self.set_cursor(parent_path) 220 | return True 221 | return False 222 | 223 | def _on_cmds_tv_button_press_event(self, treeview, event): 224 | if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: 225 | time = event.time 226 | treeview.grab_focus() 227 | sel = self.get_selection() 228 | model, rows = sel.get_selected_rows() 229 | pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y)) 230 | selected_cmds = [] 231 | 232 | if pathinfo is not None: 233 | if pathinfo[0] not in rows: 234 | # if user right-clicked on a previously unselected row, 235 | # then unselect all other rows and select only the row 236 | # under the mouse cursor 237 | path, col, cellx, celly = pathinfo 238 | treeview.grab_focus() 239 | treeview.set_cursor(path, col, 0) 240 | 241 | # build a submenu of all deputies 242 | selected_cmds = self.get_selected_commands() 243 | # can_start_stop_remove = len(selected_cmds) > 0 and \ 244 | # not self.sheriff.is_observer () 245 | 246 | else: 247 | sel.unselect_all() 248 | 249 | # enable/disable menu options based on sheriff state and user 250 | # selection 251 | can_add_load = ( 252 | not self.sheriff.is_observer() 253 | and not self.script_manager.get_active_script() 254 | ) 255 | can_modify = ( 256 | pathinfo is not None 257 | and not self.sheriff.is_observer() 258 | and not self.script_manager.get_active_script() 259 | ) 260 | 261 | self.start_cmd_ctxt_mi.set_sensitive(can_modify) 262 | self.stop_cmd_ctxt_mi.set_sensitive(can_modify) 263 | self.restart_cmd_ctxt_mi.set_sensitive(can_modify) 264 | self.remove_cmd_ctxt_mi.set_sensitive(can_modify) 265 | self.edit_cmd_ctxt_mi.set_sensitive(can_modify) 266 | self.new_cmd_ctxt_mi.set_sensitive(can_add_load) 267 | 268 | self.cmd_ctxt_menu.popup(None, None, None, None, event.button, time) 269 | return 1 270 | elif event.type == Gdk.EventType._2BUTTON_PRESS and event.button == 1: 271 | # expand or collapse groups when double clicked 272 | sel = self.get_selection() 273 | model, rows = sel.get_selected_rows() 274 | if len(rows) == 1: 275 | if model.iter_has_child(model.get_iter(rows[0])): 276 | if self.row_expanded(rows[0]): 277 | self.collapse_row(rows[0]) 278 | else: 279 | self.expand_row(rows[0], True) 280 | elif event.type == Gdk.EventType.BUTTON_PRESS and event.button == 1: 281 | # unselect all rows when the user clicks on empty space in the 282 | # commands treeview 283 | time = event.time 284 | x = int(event.x) 285 | y = int(event.y) 286 | pathinfo = treeview.get_path_at_pos(x, y) 287 | if pathinfo is None: 288 | self.get_selection().unselect_all() 289 | 290 | def _do_edit_command_dialog(self, cmds): 291 | unchanged_val = "[Unchanged]" 292 | 293 | old_deputies = [self.sheriff.get_command_deputy(cmd).deputy_id for cmd in cmds] 294 | 295 | old_exec_strs = [cmd.exec_str for cmd in cmds] 296 | old_command_ids = [cmd.command_id for cmd in cmds] 297 | old_groups = [cmd.group for cmd in cmds] 298 | old_auto_respawns = [cmd.auto_respawn for cmd in cmds] 299 | old_stop_signals = [cmd.stop_signal for cmd in cmds] 300 | old_stop_times_allowed = [cmd.stop_time_allowed for cmd in cmds] 301 | 302 | # handle all same/different deputies 303 | if all(x == old_deputies[0] for x in old_deputies): 304 | deputies_list = [deputy.deputy_id for deputy in self.sheriff.get_deputies()] 305 | cur_deputy = old_deputies[0] 306 | else: 307 | deputies_list = [unchanged_val] 308 | deputies_list.extend( 309 | [deputy.deputy_id for deputy in self.sheriff.get_deputies()] 310 | ) 311 | cur_deputy = unchanged_val 312 | 313 | # handle all same/different groups 314 | if all(x == old_groups[0] for x in old_groups): 315 | groups_list = self.cmds_ts.get_known_group_names() 316 | cur_group = old_groups[0] 317 | else: 318 | groups_list = [unchanged_val] 319 | groups_list.extend(self.cmds_ts.get_known_group_names()) 320 | cur_group = unchanged_val 321 | 322 | # executable string, command id 323 | if all(x == old_exec_strs[0] for x in old_exec_strs): 324 | cur_exec_str = old_exec_strs[0] 325 | else: 326 | cur_exec_str = unchanged_val 327 | if all(x == old_command_ids[0] for x in old_command_ids): 328 | cur_command_id = old_command_ids[0] 329 | else: 330 | cur_command_id = unchanged_val 331 | 332 | # auto respawn 333 | if all(x == old_auto_respawns[0] for x in old_auto_respawns): 334 | if old_auto_respawns[0]: 335 | cur_auto_respawn = 1 336 | else: 337 | cur_auto_respawn = 0 338 | else: 339 | cur_auto_respawn = -1 340 | 341 | # stop signal 342 | if all(x == old_stop_signals[0] for x in old_stop_signals): 343 | cur_stop_signal = old_stop_signals[0] 344 | else: 345 | cur_stop_signal = unchanged_val 346 | 347 | # stop time allowed 348 | if all(x == old_stop_times_allowed[0] for x in old_stop_times_allowed): 349 | cur_stop_time_allowed = old_stop_times_allowed[0] 350 | else: 351 | cur_stop_time_allowed = unchanged_val 352 | 353 | # create the dialog box 354 | dlg = sd.AddModifyCommandDialog( 355 | self.get_toplevel(), 356 | deputies_list, 357 | groups_list, 358 | cur_exec_str, 359 | cur_command_id, 360 | cur_deputy, 361 | cur_group, 362 | cur_auto_respawn, 363 | cur_stop_signal, 364 | cur_stop_time_allowed, 365 | is_add=False, 366 | ) 367 | 368 | while dlg.run() == Gtk.ResponseType.ACCEPT: 369 | new_exec_str = dlg.get_command() 370 | newgroup = dlg.get_group() 371 | newauto_respawn = dlg.get_auto_respawn() 372 | new_stop_signal = dlg.get_stop_signal() 373 | new_stop_time_allowed = dlg.get_stop_time_allowed() 374 | cmd_ind = 0 375 | 376 | for cmd in cmds: 377 | if new_exec_str != cmd.exec_str and new_exec_str != unchanged_val: 378 | self.sheriff.set_command_exec(cmd, new_exec_str) 379 | 380 | if newauto_respawn != cmd.auto_respawn and newauto_respawn >= 0: 381 | self.sheriff.set_command_auto_respawn(cmd, newauto_respawn) 382 | 383 | if newgroup != cmd.group and newgroup != unchanged_val: 384 | self.sheriff.set_command_group(cmd, newgroup) 385 | 386 | if ( 387 | new_stop_signal != cmd.stop_signal 388 | and new_stop_signal != unchanged_val 389 | ): 390 | self.sheriff.set_command_stop_signal(cmd, new_stop_signal) 391 | 392 | if ( 393 | new_stop_time_allowed != cmd.stop_time_allowed 394 | and new_stop_time_allowed != unchanged_val 395 | ): 396 | self.sheriff.set_command_stop_time_allowed( 397 | cmd, new_stop_time_allowed 398 | ) 399 | 400 | cmd_ind = cmd_ind + 1 401 | break 402 | dlg.destroy() 403 | 404 | def _on_cmds_tv_row_activated(self, treeview, path, column): 405 | cmd = self.cmds_ts.path_to_command(path) 406 | if not cmd: 407 | return 408 | self._do_edit_command_dialog([cmd]) 409 | 410 | def _status_cell_data_func(self, column, cell, model, model_iter, *data): 411 | color_map = { 412 | sheriff.TRYING_TO_START: "Orange", 413 | sheriff.RESTARTING: "Orange", 414 | sheriff.RUNNING: "Green", 415 | sheriff.TRYING_TO_STOP: "Yellow", 416 | sheriff.REMOVING: "Yellow", 417 | sheriff.STOPPED_ERROR: "Red", 418 | sheriff.UNKNOWN: "Gray", 419 | } 420 | 421 | assert model is self.cmds_ts 422 | cmd = self.cmds_ts.iter_to_command(model_iter) 423 | if not cmd: 424 | # group node 425 | children = self.cmds_ts.get_group_row_child_commands_recursive(model_iter) 426 | 427 | if not children: 428 | cell.set_property("cell-background-set", False) 429 | else: 430 | statuses = [cmd.status() for cmd in children] 431 | 432 | if all([s == statuses[0] for s in statuses]): 433 | # if all the commands in a group have the same status, then 434 | # color them by that status 435 | if statuses[0] == sheriff.STOPPED_OK: 436 | cell.set_property("cell-background-set", False) 437 | cell.set_property("foreground-set", False) 438 | else: 439 | cell.set_property("cell-background-set", True) 440 | cell.set_property("foreground-set", True) 441 | cell.set_property("cell-background", color_map[statuses[0]]) 442 | cell.set_property("foreground", "Black") 443 | else: 444 | # otherwise, color them yellow 445 | cell.set_property("cell-background-set", True) 446 | cell.set_property("foreground-set", True) 447 | cell.set_property("cell-background", "Yellow") 448 | cell.set_property("foreground", "Black") 449 | 450 | return 451 | 452 | if cmd.status() == sheriff.STOPPED_OK: 453 | cell.set_property("cell-background-set", False) 454 | cell.set_property("foreground-set", False) 455 | else: 456 | cell.set_property("cell-background-set", True) 457 | cell.set_property("foreground-set", True) 458 | cell.set_property("cell-background", color_map[cmd.status()]) 459 | cell.set_property("foreground", "Black") 460 | -------------------------------------------------------------------------------- /python/src/procman_ros/sheriff_gtk/deputies_treeview.py: -------------------------------------------------------------------------------- 1 | import time 2 | from gi.repository import GObject 3 | from gi.repository import Gtk 4 | from gi.repository import Gdk 5 | 6 | import procman_ros.sheriff as sheriff 7 | import procman_ros.sheriff_gtk.command_model as cm 8 | import procman_ros.sheriff_gtk.sheriff_dialogs as sd 9 | 10 | 11 | class DeputyModel(Gtk.ListStore): 12 | COL_OBJ, COL_DEPUTY_ID, COL_LAST_UPDATE, COL_LOAD, NUM_ROWS = list(range(5)) 13 | 14 | def __init__(self, _sheriff): 15 | super(DeputyModel, self).__init__( 16 | GObject.TYPE_PYOBJECT, 17 | GObject.TYPE_STRING, # deputy id 18 | GObject.TYPE_STRING, # last update time 19 | GObject.TYPE_STRING, # load 20 | ) 21 | self.sheriff = _sheriff 22 | 23 | def update(self): 24 | to_update = set(self.sheriff.get_deputies()) 25 | to_remove = [] 26 | 27 | def _deputy_last_update_str(dep): 28 | if dep.last_update_utime: 29 | now_utime = time.time() * 1000000 30 | return "{:.1f} seconds ago".format( 31 | (now_utime - dep.last_update_utime) * 1e-6 32 | ) 33 | else: 34 | return "" 35 | 36 | def _update_deputy_row(model, path, model_iter, user_data): 37 | deputy = model.get_value(model_iter, DeputyModel.COL_OBJ) 38 | if deputy in to_update: 39 | model.set( 40 | model_iter, 41 | DeputyModel.COL_LAST_UPDATE, 42 | _deputy_last_update_str(deputy), 43 | DeputyModel.COL_LOAD, 44 | "{:.4f}".format(deputy.cpu_load), 45 | ) 46 | to_update.remove(deputy) 47 | else: 48 | to_remove.append(Gtk.TreeRowReference(model, path)) 49 | 50 | self.foreach(_update_deputy_row, None) 51 | 52 | for trr in to_remove: 53 | self.remove(self.get_iter(trr.get_path())) 54 | 55 | for deputy in to_update: 56 | # print "adding %s to treeview" % deputy.deputy_id 57 | new_row = ( 58 | deputy, 59 | deputy.deputy_id, 60 | _deputy_last_update_str(deputy), 61 | "{}".format(deputy.cpu_load), 62 | ) 63 | self.append(new_row) 64 | 65 | 66 | class DeputyTreeView(Gtk.TreeView): 67 | def __init__(self, _sheriff, deputies_ts): 68 | super(DeputyTreeView, self).__init__(deputies_ts) 69 | self.sheriff = _sheriff 70 | self.deputies_ts = deputies_ts 71 | 72 | plain_tr = Gtk.CellRendererText() 73 | col = Gtk.TreeViewColumn("Deputy", plain_tr, text=DeputyModel.COL_DEPUTY_ID) 74 | col.set_sort_column_id(1) 75 | col.set_resizable(True) 76 | self.append_column(col) 77 | 78 | last_update_tr = Gtk.CellRendererText() 79 | col = Gtk.TreeViewColumn( 80 | "Last update", last_update_tr, text=DeputyModel.COL_LAST_UPDATE 81 | ) 82 | # col.set_sort_column_id (2) # XXX this triggers really weird bugs... 83 | col.set_resizable(True) 84 | col.set_cell_data_func(last_update_tr, self._deputy_last_update_cell_data_func) 85 | self.append_column(col) 86 | 87 | col = Gtk.TreeViewColumn("Load", plain_tr, text=DeputyModel.COL_LOAD) 88 | col.set_resizable(True) 89 | self.append_column(col) 90 | 91 | self.connect("button-press-event", self._on_deputies_tv_button_press_event) 92 | 93 | # deputies treeview context menu 94 | self.deputies_ctxt_menu = Gtk.Menu() 95 | 96 | self.cleanup_deputies_ctxt_mi = Gtk.MenuItem.new_with_mnemonic("_Cleanup") 97 | self.deputies_ctxt_menu.append(self.cleanup_deputies_ctxt_mi) 98 | self.cleanup_deputies_ctxt_mi.connect("activate", self._cleanup_deputies) 99 | self.deputies_ctxt_menu.show_all() 100 | 101 | def _on_deputies_tv_button_press_event(self, treeview, event): 102 | if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: 103 | self.deputies_ctxt_menu.popup(None, None, None, None, event.button, event.time) 104 | return True 105 | 106 | def _cleanup_deputies(self, *args): 107 | self.sheriff.remove_empty_deputies() 108 | self.deputies_ts.update() 109 | 110 | def _deputy_last_update_cell_data_func(self, column, cell, model, model_iter, *data): 111 | # bit of a hack to pull out the last update time 112 | try: 113 | last_update = float( 114 | model.get_value(model_iter, DeputyModel.COL_LAST_UPDATE).split()[0] 115 | ) 116 | except: 117 | last_update = None 118 | if last_update is None or last_update > 5: 119 | cell.set_property("cell-background-set", True) 120 | cell.set_property("cell-background", "Red") 121 | elif last_update > 2: 122 | cell.set_property("cell-background-set", True) 123 | cell.set_property("cell-background", "Yellow") 124 | else: 125 | cell.set_property("cell-background-set", False) 126 | 127 | def save_settings(self, save_map): 128 | pass 129 | 130 | def load_settings(self, save_map): 131 | pass 132 | -------------------------------------------------------------------------------- /python/src/procman_ros/sheriff_gtk/sheriff_dialogs.py: -------------------------------------------------------------------------------- 1 | import io as StringIO 2 | import traceback 3 | import signal 4 | 5 | from gi.repository import GObject 6 | from gi.repository import Gtk 7 | from gi.repository import Gdk 8 | 9 | from procman_ros.sheriff_config import Parser, ScriptNode 10 | from procman_ros.sheriff_script import SheriffScript, ScriptManager 11 | from procman_ros.sheriff import DEFAULT_STOP_SIGNAL, DEFAULT_STOP_TIME_ALLOWED 12 | 13 | 14 | class AddModifyCommandDialog(Gtk.Dialog): 15 | def __init__( 16 | self, 17 | parent, 18 | deputies, 19 | groups, 20 | initial_cmd="", 21 | initial_cmd_id="", 22 | initial_deputy="", 23 | initial_group="", 24 | initial_auto_respawn=False, 25 | initial_stop_signal=DEFAULT_STOP_SIGNAL, 26 | initial_stop_time_allowed=DEFAULT_STOP_TIME_ALLOWED, 27 | is_add=True, 28 | ): 29 | # add command dialog 30 | Gtk.Dialog.__init__( 31 | self, 32 | "Add/Modify Command", 33 | parent, 34 | Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 35 | ) 36 | self.add_buttons( 37 | Gtk.STOCK_OK, 38 | Gtk.ResponseType.ACCEPT, 39 | Gtk.STOCK_CANCEL, 40 | Gtk.ResponseType.REJECT, 41 | ) 42 | table = Gtk.Table(7, 2) 43 | 44 | # deputy 45 | table.attach(Gtk.Label(label="Deputy"), 0, 1, 0, 1, 0, 0) 46 | self.deputy_cb = Gtk.ComboBoxText() 47 | 48 | dep_ind = 0 49 | deputies.sort() 50 | for deputy in deputies: 51 | self.deputy_cb.append_text(deputy) 52 | if deputy == initial_deputy: 53 | self.deputy_cb.set_active(dep_ind) 54 | dep_ind += 1 55 | if self.deputy_cb.get_active() < 0 and len(deputies) > 0: 56 | self.deputy_cb.set_active(0) 57 | 58 | table.attach(self.deputy_cb, 1, 2, 0, 1) 59 | if not is_add: 60 | self.deputy_cb.set_sensitive(False) 61 | self.deputies = deputies 62 | 63 | # command id 64 | table.attach(Gtk.Label(label="Id"), 0, 1, 1, 2, 0, 0) 65 | self.cmd_id_te = Gtk.Entry() 66 | self.cmd_id_te.set_text(initial_cmd_id) 67 | self.cmd_id_te.set_width_chars(60) 68 | table.attach(self.cmd_id_te, 1, 2, 1, 2) 69 | self.cmd_id_te.connect( 70 | "activate", lambda e: self.response(Gtk.ResponseType.ACCEPT) 71 | ) 72 | if not is_add: 73 | self.cmd_id_te.set_sensitive(False) 74 | 75 | # command name 76 | table.attach(Gtk.Label(label="Command"), 0, 1, 2, 3, 0, 0) 77 | self.name_te = Gtk.Entry() 78 | self.name_te.set_text(initial_cmd) 79 | self.name_te.set_width_chars(60) 80 | table.attach(self.name_te, 1, 2, 2, 3) 81 | self.name_te.connect( 82 | "activate", lambda e: self.response(Gtk.ResponseType.ACCEPT) 83 | ) 84 | self.name_te.grab_focus() 85 | 86 | # group 87 | table.attach(Gtk.Label(label="Group"), 0, 1, 3, 4, 0, 0) 88 | self.group_cbe = Gtk.ComboBoxText.new_with_entry() 89 | # groups = groups[:] 90 | groups.sort() 91 | for group_name in groups: 92 | self.group_cbe.append_text(group_name) 93 | table.attach(self.group_cbe, 1, 2, 3, 4) 94 | self.group_cbe.get_child().set_text(initial_group) 95 | self.group_cbe.get_child().connect( 96 | "activate", lambda e: self.response(Gtk.ResponseType.ACCEPT) 97 | ) 98 | 99 | # auto respawn 100 | auto_restart_tt = "If the command terminates while running, should the deputy automatically restart it?" 101 | auto_restart_label = Gtk.Label(label="Auto-restart") 102 | auto_restart_label.set_tooltip_text(auto_restart_tt) 103 | table.attach(auto_restart_label, 0, 1, 4, 5, 0, 0) 104 | self.auto_respawn_cb = Gtk.CheckButton() 105 | self.auto_respawn_cb.set_active(initial_auto_respawn) 106 | if initial_auto_respawn < 0: 107 | self.auto_respawn_cb.set_inconsistent(True) 108 | self.auto_respawn_cb.connect("toggled", self.auto_respawn_cb_callback) 109 | self.auto_respawn_cb.set_tooltip_text(auto_restart_tt) 110 | table.attach(self.auto_respawn_cb, 1, 2, 4, 5) 111 | 112 | # stop signal 113 | stop_signal_tt = "When stopping a signal, what OS signal to initially send to request a clean exit" 114 | stop_signal_label = Gtk.Label(label="Stop signal") 115 | stop_signal_label.set_tooltip_text(stop_signal_tt) 116 | table.attach(stop_signal_label, 0, 1, 5, 6, 0, 0) 117 | try: 118 | self.stop_signal_c = Gtk.ComboBoxText() 119 | except AttributeError: 120 | self.stop_signal_c = Gtk.ComboBoxText() 121 | self.stop_signal_entries = [ 122 | (signal.SIGINT, "SIGINT"), 123 | (signal.SIGTERM, "SIGTERM"), 124 | (signal.SIGKILL, "SIGKILL"), 125 | ] 126 | for i, entry in enumerate(self.stop_signal_entries): 127 | signum, signame = entry 128 | self.stop_signal_c.append_text(signame) 129 | if signum == initial_stop_signal: 130 | self.stop_signal_c.set_active(i) 131 | self.stop_signal_c.set_tooltip_text(stop_signal_tt) 132 | table.attach(self.stop_signal_c, 1, 2, 5, 6) 133 | 134 | # stop time allowed 135 | stop_time_allowed_tt = "When stopping a running command, how long to wait between sending the stop signal and a SIGKILL if the command doesn't stop." 136 | stop_time_allowed_label = Gtk.Label(label="Time allowed when stopping") 137 | stop_time_allowed_label.set_tooltip_text(stop_time_allowed_tt) 138 | table.attach(stop_time_allowed_label, 0, 1, 6, 7, 0, 0) 139 | self.stop_time_allowed_sb = Gtk.SpinButton() 140 | self.stop_time_allowed_sb.set_increments(1, 5) 141 | self.stop_time_allowed_sb.set_range(1, 999999) 142 | self.stop_time_allowed_sb.set_value(int(initial_stop_time_allowed)) 143 | self.stop_time_allowed_sb.set_tooltip_text(stop_time_allowed_tt) 144 | table.attach(self.stop_time_allowed_sb, 1, 2, 6, 7) 145 | 146 | self.vbox.pack_start(table, False, False, 0) 147 | table.show_all() 148 | 149 | def auto_respawn_cb_callback(self, widget, data=None): 150 | if widget.get_inconsistent(): 151 | widget.set_inconsistent(False) 152 | 153 | def get_deputy(self): 154 | model = self.deputy_cb.get_model() 155 | active = self.deputy_cb.get_active() 156 | if active < 0: 157 | return None 158 | return model[active][0] 159 | 160 | def get_command(self): 161 | return self.name_te.get_text() 162 | 163 | def get_command_id(self): 164 | return self.cmd_id_te.get_text().strip() 165 | 166 | def get_group(self): 167 | return self.group_cbe.get_child().get_text().strip() 168 | 169 | def get_auto_respawn(self): 170 | if self.auto_respawn_cb.get_inconsistent(): 171 | return -1 172 | else: 173 | return self.auto_respawn_cb.get_active() 174 | 175 | def get_stop_signal(self): 176 | return self.stop_signal_entries[self.stop_signal_c.get_active()][0] 177 | 178 | def get_stop_time_allowed(self): 179 | return self.stop_time_allowed_sb.get_value() 180 | 181 | 182 | class PreferencesDialog(Gtk.Dialog): 183 | def __init__(self, sheriff_gtk, parent): 184 | # add command dialog 185 | Gtk.Dialog.__init__( 186 | self, 187 | "Preferences", 188 | parent, 189 | Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 190 | ) 191 | self.add_buttons( 192 | Gtk.STOCK_OK, 193 | Gtk.ResponseType.ACCEPT, 194 | Gtk.STOCK_CANCEL, 195 | Gtk.ResponseType.REJECT, 196 | ) 197 | 198 | table = Gtk.Table(4, 2) 199 | 200 | # console rate limit 201 | table.attach(Gtk.Label(label="Console rate limit (kB/s)"), 0, 1, 0, 1, 0, 0) 202 | self.rate_limit_sb = Gtk.SpinButton() 203 | self.rate_limit_sb.set_digits(0) 204 | self.rate_limit_sb.set_increments(1, 1000) 205 | self.rate_limit_sb.set_range(0, 999999) 206 | self.rate_limit_sb.set_value(sheriff_gtk.cmd_console.get_output_rate_limit()) 207 | 208 | table.attach(self.rate_limit_sb, 1, 2, 0, 1) 209 | 210 | # background color 211 | table.attach(Gtk.Label(label="Console background color"), 0, 1, 1, 2, 0, 0) 212 | self.bg_color_bt = Gtk.ColorButton.new_with_rgba( 213 | Gdk.RGBA.from_color(sheriff_gtk.cmd_console.get_background_color()) 214 | ) 215 | table.attach(self.bg_color_bt, 1, 2, 1, 2) 216 | 217 | # foreground color 218 | table.attach(Gtk.Label(label="Console text color"), 0, 1, 2, 3, 0, 0) 219 | self.text_color_bt = Gtk.ColorButton.new_with_rgba( 220 | Gdk.RGBA.from_color(sheriff_gtk.cmd_console.get_text_color()) 221 | ) 222 | table.attach(self.text_color_bt, 1, 2, 2, 3) 223 | 224 | # font 225 | table.attach(Gtk.Label(label="Console font"), 0, 1, 3, 4, 0, 0) 226 | self.font_bt = Gtk.FontButton.new_with_font(sheriff_gtk.cmd_console.get_font()) 227 | table.attach(self.font_bt, 1, 2, 3, 4) 228 | 229 | self.vbox.pack_start(table, False, False, 0) 230 | table.show_all() 231 | 232 | 233 | def do_add_command_dialog(sheriff, cmds_ts, window): 234 | deputies = sheriff.get_deputies() 235 | if not deputies: 236 | msgdlg = Gtk.MessageDialog( 237 | window, 238 | Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 239 | Gtk.MessageType.ERROR, 240 | Gtk.ButtonsType.CLOSE, 241 | "Can't add a command without an active deputy", 242 | ) 243 | msgdlg.run() 244 | msgdlg.destroy() 245 | return 246 | deputy_ids = [deputy.deputy_id for deputy in deputies] 247 | 248 | # pick an initial command id 249 | existing_ids = {cmd.command_id for cmd in sheriff.get_all_commands()} 250 | initial_cmd_id = "" 251 | for i in range(len(existing_ids) + 1): 252 | initial_cmd_id = "command_{}".format(i) 253 | if initial_cmd_id not in existing_ids: 254 | break 255 | assert initial_cmd_id and initial_cmd_id not in existing_ids 256 | 257 | dlg = AddModifyCommandDialog( 258 | window, 259 | deputy_ids, 260 | cmds_ts.get_known_group_names(), 261 | initial_cmd_id=initial_cmd_id, 262 | ) 263 | 264 | while dlg.run() == Gtk.ResponseType.ACCEPT: 265 | try: 266 | sheriff.add_command( 267 | dlg.get_command_id(), 268 | dlg.get_deputy(), 269 | dlg.get_command(), 270 | dlg.get_group().strip(), 271 | dlg.get_auto_respawn(), 272 | dlg.get_stop_signal(), 273 | dlg.get_stop_time_allowed(), 274 | ) 275 | break 276 | except ValueError as xcp: 277 | msgdlg = Gtk.MessageDialog( 278 | window, 279 | Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 280 | Gtk.MessageType.ERROR, 281 | Gtk.ButtonsType.CLOSE, 282 | str(xcp), 283 | ) 284 | msgdlg.run() 285 | msgdlg.destroy() 286 | dlg.destroy() 287 | 288 | 289 | class AddModifyScriptDialog(Gtk.Dialog): 290 | def __init__(self, parent, script): 291 | # add command dialog 292 | title = "Edit script" 293 | if script is None: 294 | title = "New script" 295 | Gtk.Dialog.__init__( 296 | self, 297 | title, 298 | parent, 299 | Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 300 | ) 301 | self.add_buttons( 302 | Gtk.STOCK_OK, 303 | Gtk.ResponseType.ACCEPT, 304 | Gtk.STOCK_CANCEL, 305 | Gtk.ResponseType.REJECT, 306 | ) 307 | 308 | self.set_default_size(800, 400) 309 | 310 | hbox = Gtk.HBox() 311 | 312 | default_contents = 'script "script-name" {\n' " # script commands here\n" "}" 313 | 314 | # script contents 315 | self.script_tv = Gtk.TextView() 316 | self.script_tv.set_editable(True) 317 | self.script_tv.set_accepts_tab(False) 318 | if script is not None: 319 | self.script_tv.get_buffer().set_text(str(script)) 320 | else: 321 | self.script_tv.get_buffer().set_text(default_contents) 322 | sw = Gtk.ScrolledWindow() 323 | sw.add(self.script_tv) 324 | hbox.pack_start(sw, True, True, 0) 325 | if script is not None: 326 | self.script_tv.grab_focus() 327 | 328 | # # Help text 329 | help_tv = Gtk.TextView() 330 | help_tv.set_editable(False) 331 | help_tv.set_sensitive(False) 332 | help_tv.get_buffer().set_text( 333 | """ 334 | Example commands: 335 | start cmd "server" wait "running"; 336 | start cmd "client"; 337 | stop cmd "server" wait "stopped"; 338 | restart group "mygroup"; 339 | wait group "mygroup" status "running"; 340 | wait ms 500; 341 | run_script "other-script-name"; 342 | """ 343 | ) 344 | 345 | # Refer to commands and groups by what appears in the Name column. 346 | # Valid actions are: 347 | # start|stop|restart cmd|group "cmd_id" [wait "running"|"stopped"]; 348 | # wait ms ###; 349 | # run_script "other-script-name"; 350 | 351 | hbox.pack_start(help_tv, False, False, 0) 352 | self.vbox.pack_start(hbox, True, True, 0) 353 | hbox.show_all() 354 | 355 | # def get_script_name (self): return self.name_te.get_text () 356 | def get_script_contents(self): 357 | buf = self.script_tv.get_buffer() 358 | return buf.get_text(buf.get_start_iter(), buf.get_end_iter(), True) 359 | 360 | 361 | def _do_err_dialog(window, msg): 362 | msgdlg = Gtk.MessageDialog( 363 | window, 364 | Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 365 | Gtk.MessageType.ERROR, 366 | Gtk.ButtonsType.CLOSE, 367 | ) 368 | msgdlg.set_markup('{}'.format(msg)) 369 | msgdlg.run() 370 | msgdlg.destroy() 371 | 372 | 373 | def _parse_script(script_manager, window, dlg): 374 | contents = dlg.get_script_contents() 375 | 376 | # check script for errors 377 | parser = Parser() 378 | try: 379 | cfg_node = parser.parse(StringIO.StringIO(contents)) 380 | except ValueError as xcp: 381 | # traceback.print_exc() 382 | _do_err_dialog(window, str(xcp)) 383 | return None 384 | 385 | script_nodes = list(cfg_node.scripts.values()) 386 | if not script_nodes: 387 | _do_err_dialog(window, "That's not a script...") 388 | return None 389 | 390 | if len(script_nodes) > 1: 391 | _do_err_dialog(window, "Only one script {} stanza allowed!") 392 | return None 393 | 394 | script = SheriffScript.from_script_node(script_nodes[0]) 395 | 396 | errors = script_manager.check_script_for_errors(script) 397 | if errors: 398 | print(errors) 399 | _do_err_dialog(window, "Script error.\n\n" + "\n ".join(errors)) 400 | return None 401 | return script 402 | 403 | 404 | def do_add_script_dialog(script_manager, window): 405 | dlg = AddModifyScriptDialog(window, None) 406 | while dlg.run() == Gtk.ResponseType.ACCEPT: 407 | script = _parse_script(script_manager, window, dlg) 408 | if script is None: 409 | dlg.script_tv.grab_focus() 410 | continue 411 | if script_manager.get_script(script.name) is not None: 412 | _do_err_dialog( 413 | window, "A script named {} already exists!".format(script.name) 414 | ) 415 | continue 416 | script_manager.add_script(script) 417 | break 418 | dlg.destroy() 419 | 420 | 421 | def do_edit_script_dialog(script_manager, window, script): 422 | if script_manager.get_active_script(): 423 | _do_err_dialog( 424 | window, "Script editing is not allowed while a script is running." 425 | ) 426 | return 427 | 428 | dlg = AddModifyScriptDialog(window, script) 429 | while dlg.run() == Gtk.ResponseType.ACCEPT: 430 | new_script = _parse_script(script_manager, window, dlg) 431 | if new_script is None: 432 | dlg.script_tv.grab_focus() 433 | continue 434 | if new_script.name != script.name: 435 | if script_manager.get_script(new_script.name) is not None: 436 | _do_err_dialog( 437 | window, "A script named {} already exists!".format(script.name) 438 | ) 439 | dlg.script_tv.grab_focus() 440 | continue 441 | script_manager.remove_script(script) 442 | script_manager.add_script(new_script) 443 | break 444 | dlg.destroy() 445 | 446 | 447 | def do_preferences_dialog(sheriff_gtk, window): 448 | dlg = PreferencesDialog(sheriff_gtk, window) 449 | 450 | if dlg.run() == Gtk.ResponseType.ACCEPT: 451 | sheriff_gtk.cmd_console.set_background_color(dlg.bg_color_bt.get_color()) 452 | sheriff_gtk.cmd_console.set_text_color(dlg.text_color_bt.get_color()) 453 | sheriff_gtk.cmd_console.set_output_rate_limit( 454 | dlg.rate_limit_sb.get_value_as_int() 455 | ) 456 | sheriff_gtk.cmd_console.set_font(dlg.font_bt.get_font_name()) 457 | 458 | # sheriff_Gtk.cmds_tv.set_background_color(dlg.bg_color_bt.get_color()) 459 | # sheriff_Gtk.cmds_tv.set_text_color(dlg.text_color_bt.get_color()) 460 | # 461 | # sheriff_Gtk.deputies_tv.set_background_color(dlg.bg_color_bt.get_color()) 462 | # sheriff_Gtk.deputies_tv.set_text_color(dlg.text_color_bt.get_color()) 463 | 464 | dlg.destroy() 465 | -------------------------------------------------------------------------------- /scripts/sheriff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import procman_ros.sheriff_gtk.sheriff_gtk 3 | procman_ros.sheriff_gtk.sheriff_gtk.main() 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ## ! DO NOT MANUALLY INVOKE THIS setup.py, USE CATKIN INSTEAD 2 | 3 | from setuptools import setup 4 | from catkin_pkg.python_setup import generate_distutils_setup 5 | 6 | # fetch values from package.xml 7 | setup_args = generate_distutils_setup( 8 | packages=['procman_ros', 'procman_ros.sheriff_gtk'], 9 | package_dir={'': 'python/src'}) 10 | 11 | setup(**setup_args) -------------------------------------------------------------------------------- /src/procman/exec_string_utils.cpp: -------------------------------------------------------------------------------- 1 | #include "procman/exec_string_utils.hpp" 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace procman { 8 | 9 | std::vector Split(const std::string& input, 10 | const std::string& delimeters, 11 | int max_items) { 12 | std::vector result; 13 | 14 | int tok_begin = 0; 15 | int tok_end = 0; 16 | while (tok_begin < input.size()) { 17 | if (result.size() == max_items - 1) { 18 | result.emplace_back(&input[tok_begin]); 19 | return result; 20 | } 21 | 22 | for (tok_end = tok_begin; 23 | tok_end < input.size() && 24 | !strchr(delimeters.c_str(), input[tok_end]); 25 | ++tok_end) {} 26 | 27 | result.emplace_back(&input[tok_begin], tok_end - tok_begin); 28 | 29 | tok_begin = tok_end + 1; 30 | } 31 | 32 | return result; 33 | } 34 | 35 | void Strfreev(char** vec) { 36 | for (char** ptr = vec; *ptr; ++ptr) { 37 | free(*ptr); 38 | } 39 | free(vec); 40 | } 41 | 42 | class VariableExpander { 43 | public: 44 | VariableExpander(const std::string& input) : 45 | input_(input), 46 | pos_(0) { 47 | Process(); 48 | } 49 | 50 | const std::string Result() const { return output_.str(); } 51 | 52 | private: 53 | void Process() { 54 | while(NextChar()) { 55 | const char c = cur_char_; 56 | if('\\' == c) { 57 | if(NextChar()) { 58 | output_.put(c); 59 | } else { 60 | output_.put('\\'); 61 | } 62 | continue; 63 | } 64 | // variable? 65 | if('$' == c) { 66 | ParseVariable(); 67 | } else { 68 | output_.put(c); 69 | } 70 | } 71 | } 72 | 73 | bool HasChar() const { 74 | return pos_ < input_.size(); 75 | } 76 | 77 | char PeekChar() const { 78 | return HasChar() ? input_[pos_] : 0; 79 | } 80 | 81 | bool NextChar() { 82 | if(HasChar()) { 83 | cur_char_ = input_[pos_]; 84 | pos_++; 85 | return true; 86 | } else { 87 | cur_char_ = 0; 88 | return false; 89 | } 90 | } 91 | 92 | bool ParseVariable() { 93 | int start = pos_; 94 | if(!HasChar()) { 95 | output_.put('$'); 96 | return false; 97 | } 98 | int has_braces = PeekChar() == '{'; 99 | if(has_braces) { 100 | NextChar(); 101 | } 102 | int varname_start = pos_; 103 | int varname_len = 0; 104 | while(HasChar() && 105 | IsValidVariableCharacter(PeekChar(), varname_len)) { 106 | varname_len++; 107 | NextChar(); 108 | } 109 | char* varname = strndup(&input_[varname_start], varname_len); 110 | bool braces_ok = true; 111 | if(has_braces && ((!NextChar()) || cur_char_ != '}')) { 112 | braces_ok = false; 113 | } 114 | bool ok = varname_len && braces_ok; 115 | if (ok) { 116 | // lookup the environment variable 117 | const char* val = getenv(varname); 118 | if (val) { 119 | output_ << val; 120 | } else { 121 | ok = false; 122 | } 123 | } 124 | if (!ok) { 125 | output_.write(&input_[start - 1], pos_ - start + 1); 126 | } 127 | free(varname); 128 | return ok; 129 | } 130 | 131 | static bool IsValidVariableCharacter(char c, int pos) { 132 | const char* valid_start = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"; 133 | const char* valid_follow = "1234567890"; 134 | return (strchr(valid_start, c) != NULL || 135 | ((pos > 0) && (strchr(valid_follow, c) != NULL))); 136 | } 137 | 138 | const std::string& input_; 139 | int pos_; 140 | char cur_char_; 141 | std::stringstream output_; 142 | }; 143 | 144 | std::string ExpandVariables(const std::string& input) { 145 | return VariableExpander(input).Result(); 146 | } 147 | 148 | class ArgSeparator { 149 | public: 150 | ArgSeparator(const std::string& input) : 151 | input_(input), 152 | pos_(0) { 153 | Process(); 154 | } 155 | 156 | std::vector Result() { 157 | return result_; 158 | } 159 | 160 | private: 161 | void Process() { 162 | while (ParseArg()) { 163 | result_.emplace_back(&cur_arg_[0], cur_arg_.size()); 164 | } 165 | } 166 | 167 | bool ParseArg() { 168 | cur_arg_.clear(); 169 | 170 | // Skip leading whitespace 171 | while (NextChar() && std::isspace(cur_char_)) {} 172 | if (!HasChar()) { 173 | return false; 174 | } 175 | 176 | bool quoted = false; 177 | char quote_char = 0; 178 | do { 179 | if (cur_char_ == '\\') { 180 | cur_arg_.push_back(NextChar() ? cur_char_ : '\\'); 181 | } else if (!quoted && cur_char_ == '\'') { 182 | quoted = true; 183 | quote_char = '\''; 184 | } else if (!quoted && cur_char_ == '"') { 185 | quoted = true; 186 | quote_char = '"'; 187 | } else if (quoted && quote_char == cur_char_) { 188 | quoted = false; 189 | } else if (!quoted && std::isspace(cur_char_)) { 190 | return true; 191 | } else { 192 | cur_arg_.push_back(cur_char_); 193 | } 194 | } while (NextChar()); 195 | return true; 196 | } 197 | 198 | bool HasChar() const { 199 | return pos_ < input_.size(); 200 | } 201 | 202 | bool NextChar() { 203 | if(HasChar()) { 204 | cur_char_ = input_[pos_]; 205 | pos_++; 206 | return true; 207 | } else { 208 | cur_char_ = 0; 209 | return false; 210 | } 211 | } 212 | 213 | const std::string& input_; 214 | int pos_; 215 | char cur_char_; 216 | std::vector result_; 217 | std::vector cur_arg_; 218 | }; 219 | 220 | std::vector SeparateArgs(const std::string& input) { 221 | return ArgSeparator(input).Result(); 222 | } 223 | 224 | } // namespace procman 225 | -------------------------------------------------------------------------------- /src/procman/procinfo_generic.cpp: -------------------------------------------------------------------------------- 1 | #include "procinfo.hpp" 2 | 3 | #include 4 | 5 | namespace procman { 6 | 7 | std::vector GetDescendants(int pid) { 8 | return std::vector(); 9 | } 10 | 11 | bool IsOrphanedChildOf(int orphan, int parent) { 12 | return false; 13 | } 14 | 15 | bool ReadProcessInfo(int pid, ProcessInfo *procinfo) { 16 | memset(procinfo, 0, sizeof(ProcessInfo)); 17 | return false; 18 | } 19 | 20 | bool ReadSystemInfo(SystemInfo *sysinfo) { 21 | memset(sysinfo, 0, sizeof(SystemInfo)); 22 | return false; 23 | } 24 | 25 | } // namespace procman 26 | -------------------------------------------------------------------------------- /src/procman/procinfo_linux.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * code for reading detailed process information on a GNU/Llinux system 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #include "procman/procinfo.hpp" 19 | 20 | namespace procman { 21 | 22 | static void strsplit(char *buf, char **words, int maxwords) { 23 | int inword = 0; 24 | int i; 25 | int wordind = 0; 26 | for (i = 0; buf[i] != 0; i++) { 27 | if (isspace(buf[i])) { 28 | inword = 0; 29 | buf[i] = 0; 30 | } else { 31 | if (!inword) { 32 | words[wordind] = buf + i; 33 | wordind++; 34 | if (wordind >= maxwords) 35 | break; 36 | inword = 1; 37 | } 38 | } 39 | } 40 | words[wordind] = NULL; 41 | } 42 | 43 | struct PidInfo { 44 | int pid; 45 | int ppid; 46 | int pgrp; 47 | int session; 48 | char state; 49 | 50 | std::vector children; 51 | }; 52 | 53 | bool GetPidInfo(int pid, PidInfo *result) { 54 | result->pid = pid; 55 | result->children.clear(); 56 | 57 | char fname[40]; 58 | snprintf(fname, 40, "/proc/%d/stat", pid); 59 | FILE *fp = fopen(fname, "r"); 60 | if (!fp) { 61 | return false; 62 | } 63 | int read_pid; 64 | char exec_name[PATH_MAX + 1]; 65 | int numwords = 66 | fscanf(fp, "%d %s %c %d %d %d", &read_pid, exec_name, &result->state, 67 | &result->ppid, &result->pgrp, &result->session); 68 | fclose(fp); 69 | if (6 != numwords) { 70 | return false; 71 | } 72 | return true; 73 | } 74 | 75 | bool IsOrphanedChildOf(int orphan, int parent) { 76 | PidInfo pinfo; 77 | if (!GetPidInfo(orphan, &pinfo)) { 78 | return false; 79 | } 80 | return (pinfo.ppid == 1 && pinfo.pgrp == parent && pinfo.session == parent); 81 | } 82 | 83 | static std::map get_all_pids_and_ppids() { 84 | std::map result; 85 | DIR *procdir = opendir("/proc"); 86 | if (!procdir) { 87 | return result; 88 | } 89 | 90 | struct dirent *entry; 91 | for (entry = readdir(procdir); entry; entry = readdir(procdir)) { 92 | const int pid = atoi(entry->d_name); 93 | if (!pid) { 94 | continue; 95 | } 96 | result[pid] = PidInfo(); 97 | PidInfo &pinfo = result[pid]; 98 | if (!GetPidInfo(pid, &pinfo)) { 99 | continue; 100 | } 101 | auto iter = result.find(pinfo.ppid); 102 | if (iter != result.end()) { 103 | PidInfo &parent = iter->second; 104 | parent.children.push_back(&pinfo); 105 | } 106 | } 107 | closedir(procdir); 108 | return result; 109 | } 110 | 111 | static void pid_info_get_descendants(const PidInfo &pinfo, 112 | std::vector *result) { 113 | for (PidInfo *child : pinfo.children) { 114 | assert(child->ppid == pinfo.pid); 115 | result->push_back(child->pid); 116 | pid_info_get_descendants(*child, result); 117 | } 118 | } 119 | 120 | std::vector GetDescendants(int pid) { 121 | std::vector result; 122 | 123 | std::map pid_graph = get_all_pids_and_ppids(); 124 | auto iter = pid_graph.find(pid); 125 | if (iter != pid_graph.end()) { 126 | PidInfo &root = iter->second; 127 | pid_info_get_descendants(root, &result); 128 | } 129 | return result; 130 | } 131 | 132 | bool ReadProcessInfoWithChildren(int pid, ProcessInfo *procinfo) { 133 | if (!ReadProcessInfo(pid, procinfo)) { 134 | return false; 135 | } 136 | 137 | std::vector child_pids = GetDescendants(pid); 138 | 139 | for (int child_pid : child_pids) { 140 | ProcessInfo info; 141 | ReadProcessInfo(child_pid, &info); 142 | procinfo->user += info.user; 143 | procinfo->system += info.system; 144 | procinfo->vsize += info.vsize; 145 | procinfo->rss += info.rss; 146 | procinfo->shared += info.shared; 147 | procinfo->data += info.data; 148 | procinfo->text += info.text; 149 | } 150 | 151 | return true; 152 | } 153 | 154 | bool ReadProcessInfo(int pid, ProcessInfo *procinfo) { 155 | memset(procinfo, 0, sizeof(ProcessInfo)); 156 | char fname[80]; 157 | sprintf(fname, "/proc/%d/stat", pid); 158 | FILE *fp = fopen(fname, "r"); 159 | if (!fp) { 160 | return false; 161 | } 162 | 163 | char buf[4096]; 164 | if (!fgets(buf, sizeof(buf), fp)) { 165 | return false; 166 | } 167 | char *words[50]; 168 | memset(words, 0, sizeof(words)); 169 | strsplit(buf, words, 50); 170 | 171 | procinfo->user = atoi(words[13]); 172 | procinfo->system = atoi(words[14]); 173 | procinfo->vsize = strtoll(words[22], NULL, 10); 174 | procinfo->rss = strtoll(words[23], NULL, 10) * getpagesize(); 175 | 176 | fclose(fp); 177 | 178 | sprintf(fname, "/proc/%d/statm", pid); 179 | fp = fopen(fname, "r"); 180 | if (!fp) { 181 | return false; 182 | } 183 | 184 | if (!fgets(buf, sizeof(buf), fp)) { 185 | return false; 186 | } 187 | memset(words, 0, sizeof(words)); 188 | strsplit(buf, words, 50); 189 | 190 | procinfo->shared = atoi(words[2]) * getpagesize(); 191 | procinfo->text = atoi(words[3]) * getpagesize(); 192 | procinfo->data = atoi(words[5]) * getpagesize(); 193 | 194 | fclose(fp); 195 | return true; 196 | } 197 | 198 | bool ReadSystemInfo(SystemInfo *sysinfo) { 199 | memset(sysinfo, 0, sizeof(SystemInfo)); 200 | FILE *fp = fopen("/proc/stat", "r"); 201 | if (!fp) { 202 | return false; 203 | } 204 | 205 | char buf[4096]; 206 | char tmp[80]; 207 | 208 | while (!feof(fp)) { 209 | if (!fgets(buf, sizeof(buf), fp)) { 210 | if (feof(fp)) 211 | break; 212 | else 213 | return false; 214 | } 215 | 216 | if (!strncmp(buf, "cpu ", 4)) { 217 | sscanf(buf, "%s %u %u %u %u", tmp, &sysinfo->user, &sysinfo->user_low, 218 | &sysinfo->system, &sysinfo->idle); 219 | break; 220 | } 221 | } 222 | fclose(fp); 223 | 224 | fp = fopen("/proc/meminfo", "r"); 225 | if (!fp) { 226 | return false; 227 | } 228 | while (!feof(fp)) { 229 | char units[10]; 230 | memset(units, 0, sizeof(units)); 231 | if (!fgets(buf, sizeof(buf), fp)) { 232 | if (feof(fp)) { 233 | break; 234 | } else { 235 | return false; 236 | } 237 | } 238 | 239 | if (!strncmp("MemTotal:", buf, strlen("MemTotal:"))) { 240 | sscanf(buf, "MemTotal: %" PRId64 " %9s", &sysinfo->memtotal, units); 241 | sysinfo->memtotal *= 1024; 242 | } else if (!strncmp("MemFree:", buf, strlen("MemFree:"))) { 243 | sscanf(buf, "MemFree: %" PRId64 " %9s", &sysinfo->memfree, units); 244 | sysinfo->memfree *= 1024; 245 | } else if (!strncmp("SwapTotal:", buf, strlen("SwapTotal:"))) { 246 | sscanf(buf, "SwapTotal: %" PRId64 " %9s", &sysinfo->swaptotal, units); 247 | sysinfo->swaptotal *= 1024; 248 | } else if (!strncmp("SwapFree:", buf, strlen("SwapFree:"))) { 249 | sscanf(buf, "SwapFree: %" PRId64 " %9s", &sysinfo->swapfree, units); 250 | sysinfo->swapfree *= 1024; 251 | } else { 252 | continue; 253 | } 254 | 255 | if (0 != strcmp(units, "kB")) { 256 | fprintf(stderr, 257 | "unknown units [%s] while reading " 258 | "/proc/meminfo!!!\n", 259 | units); 260 | } 261 | } 262 | 263 | fclose(fp); 264 | 265 | return true; 266 | } 267 | 268 | } // namespace procman 269 | -------------------------------------------------------------------------------- /src/procman/procman.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #ifdef __APPLE__ 14 | #include 15 | #else 16 | #include 17 | #endif 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | #include "procman/exec_string_utils.hpp" 26 | #include "procman/procman.hpp" 27 | #include "procman/procinfo.hpp" 28 | 29 | namespace procman { 30 | 31 | Procman::Procman() { 32 | } 33 | 34 | Procman::~Procman() { 35 | while (!commands_.empty()) { 36 | RemoveCommand(commands_.front()); 37 | } 38 | } 39 | 40 | void Procman::StartCommand(ProcmanCommandPtr cmd) { 41 | if (0 != cmd->Pid()) { 42 | // Command is already running. 43 | return; 44 | } 45 | ROS_DEBUG("starting [%s]\n", cmd->ExecStr().c_str()); 46 | 47 | cmd->PrepareArgsAndEnvironment(); 48 | 49 | // close existing fd's 50 | if (cmd->StdoutFd() >= 0) { 51 | close (cmd->StdoutFd()); 52 | cmd->SetStdoutFd(-1); 53 | } 54 | cmd->SetStdinFd(-1); 55 | cmd->SetExitStatus(0); 56 | 57 | // make a backup of stderr, in case something bad happens during exec. 58 | // if exec succeeds, then we have a dangling file descriptor that 59 | // gets closed when the child exits... that's okay 60 | const int stderr_backup = dup(STDERR_FILENO); 61 | 62 | int stdin_fd; 63 | const int pid = forkpty(&stdin_fd, NULL, NULL, NULL); 64 | cmd->SetStdinFd(stdin_fd); 65 | if (0 == pid) { 66 | // set environment variables from the beginning of the command 67 | for (auto& item : cmd->environment_) { 68 | setenv(item.first.c_str(), item.second.c_str(), 1); 69 | } 70 | 71 | // go! 72 | execvp(cmd->argv_[0], cmd->argv_); 73 | 74 | char ebuf[1024]; 75 | snprintf (ebuf, sizeof(ebuf), "%s", strerror(errno)); 76 | fprintf(stderr, "ERRROR executing [%s]\n", cmd->ExecStr().c_str()); 77 | fprintf(stderr, " execv: %s\n", ebuf); 78 | 79 | // if execv returns, the command did not execute successfully 80 | // (e.g. permission denied or bad path or something) 81 | 82 | // restore stderr so we can barf a real error message 83 | close(STDERR_FILENO); 84 | dup2(stderr_backup, STDERR_FILENO); 85 | fprintf(stderr, "ERROR executing [%s]\n", cmd->ExecStr().c_str()); 86 | fprintf(stderr, " execv: %s\n", ebuf); 87 | close(stderr_backup); 88 | 89 | exit(-1); 90 | } else if (pid < 0) { 91 | const std::string errmsg(strerror(errno)); 92 | close(stderr_backup); 93 | throw std::runtime_error("forkpty: " + errmsg); 94 | } else { 95 | cmd->SetPid(pid); 96 | cmd->SetStdoutFd(cmd->StdinFd()); 97 | close(stderr_backup); 98 | } 99 | } 100 | 101 | bool Procman::KillCommand(ProcmanCommandPtr cmd, int signum) { 102 | if (0 == cmd->Pid()) { 103 | ROS_DEBUG ("[%s] has no PID. not stopping (already dead)\n", cmd->ExecStr().c_str()); 104 | return false; 105 | } 106 | // get a list of the process's descendants 107 | std::vector descendants = GetDescendants(cmd->Pid()); 108 | 109 | ROS_DEBUG ("[%s] stop (signal %d)\n", cmd->ExecStr().c_str(), signum); 110 | if (0 != kill (cmd->Pid(), signum)) { 111 | return false; 112 | } 113 | 114 | // send the same signal to all of the process's descendants 115 | for (int child_pid : descendants) { 116 | ROS_DEBUG("signal %d to descendant %d\n", signum, child_pid); 117 | kill(child_pid, signum); 118 | 119 | auto iter = std::find(cmd->descendants_to_kill_.begin(), 120 | cmd->descendants_to_kill_.end(), child_pid); 121 | if (iter == cmd->descendants_to_kill_.end()) { 122 | cmd->descendants_to_kill_.push_back(child_pid); 123 | } 124 | } 125 | return true; 126 | } 127 | 128 | ProcmanCommandPtr Procman::CheckForStoppedCommands() { 129 | int exit_status; 130 | for (int pid = waitpid(-1, &exit_status, WNOHANG); 131 | pid > 0; 132 | pid = waitpid(-1, &exit_status, WNOHANG)) { 133 | for (ProcmanCommandPtr cmd : commands_) { 134 | if(pid != cmd->Pid()) { 135 | continue; 136 | } 137 | cmd->SetPid(0); 138 | cmd->SetExitStatus(exit_status); 139 | 140 | if (WIFSIGNALED (exit_status)) { 141 | int signum = WTERMSIG (exit_status); 142 | ROS_DEBUG ("[%s] terminated by signal %d (%s)\n", 143 | cmd->ExecStr().c_str(), signum, strsignal (signum)); 144 | } else if (exit_status != 0) { 145 | ROS_DEBUG ("[%s] exited with status %d\n", 146 | cmd->ExecStr().c_str(), WEXITSTATUS (exit_status)); 147 | } else { 148 | ROS_DEBUG ("[%s] exited\n", cmd->ExecStr().c_str()); 149 | } 150 | 151 | // check for and kill orphaned children. 152 | for (int child_pid : cmd->descendants_to_kill_) { 153 | if(IsOrphanedChildOf(child_pid, pid)) { 154 | ROS_DEBUG("sending SIGKILL to orphan process %d\n", child_pid); 155 | kill(child_pid, SIGKILL); 156 | } 157 | } 158 | 159 | dead_children_.push_back(cmd); 160 | break; 161 | } 162 | } 163 | 164 | if (!dead_children_.empty()) { 165 | return dead_children_.front(); 166 | } 167 | return ProcmanCommandPtr(); 168 | } 169 | 170 | void Procman::CleanupStoppedCommand(ProcmanCommandPtr cmd) { 171 | auto iter = std::find(dead_children_.begin(), dead_children_.end(), cmd); 172 | if (iter == dead_children_.end()) { 173 | return; 174 | } 175 | dead_children_.erase(iter); 176 | 177 | if (cmd->StdoutFd() < 0 && cmd->StdinFd() < 0) { 178 | return; 179 | } 180 | if (cmd->StdoutFd() >= 0) { 181 | close(cmd->StdoutFd()); 182 | } 183 | cmd->SetStdinFd(-1); 184 | cmd->SetStdoutFd(-1); 185 | assert(!cmd->Pid()); 186 | } 187 | 188 | void ProcmanCommand::PrepareArgsAndEnvironment() { 189 | if (argv_) { 190 | Strfreev(argv_); 191 | argv_ = NULL; 192 | } 193 | environment_.clear(); 194 | 195 | const std::vector args = SeparateArgs(exec_str_); 196 | 197 | // Extract environment variables and expand variables 198 | argv_ = (char**) calloc(args.size() + 1, sizeof(char**)); 199 | int num_env_vars = 0; 200 | for (int i = 0; i < args.size(); i++) { 201 | if (i == num_env_vars && strchr(args[i].c_str(), '=')) { 202 | const std::vector parts = Split(args[i], "=", 2); 203 | environment_[parts[0]] = parts[1]; 204 | ++num_env_vars; 205 | } else { 206 | // substitute variables 207 | const std::string arg = ExpandVariables(args[i]); 208 | argv_[i - num_env_vars] = strdup(arg.c_str()); 209 | } 210 | } 211 | argc_ = args.size() - num_env_vars; 212 | } 213 | 214 | ProcmanCommand::ProcmanCommand(const std::string& exec_str) : 215 | exec_str_(exec_str), 216 | pid_(0), 217 | stdin_fd_(-1), 218 | stdout_fd_(-1), 219 | exit_status_(0), 220 | argc_(0), 221 | argv_(nullptr) {} 222 | 223 | ProcmanCommand::~ProcmanCommand() { 224 | if (argv_) { 225 | Strfreev (argv_); 226 | } 227 | } 228 | 229 | const std::vector& Procman::GetCommands() { 230 | return commands_; 231 | } 232 | 233 | ProcmanCommandPtr Procman::AddCommand(const std::string& exec_str) { 234 | ProcmanCommandPtr newcmd(new ProcmanCommand(exec_str)); 235 | commands_.push_back(newcmd); 236 | ROS_DEBUG("new command [%s]\n", exec_str.c_str()); 237 | return newcmd; 238 | } 239 | 240 | void Procman::RemoveCommand(ProcmanCommandPtr cmd) { 241 | CheckCommand(cmd); 242 | 243 | // Wait for the command to exit. 244 | while(cmd->Pid()) { 245 | usleep(1000); 246 | CheckForStoppedCommands(); 247 | } 248 | 249 | CleanupStoppedCommand(cmd); 250 | 251 | // remove 252 | commands_.erase(std::find(commands_.begin(), commands_.end(), cmd)); 253 | } 254 | 255 | CommandStatus Procman::GetCommandStatus(ProcmanCommandPtr cmd) { 256 | if (cmd->Pid() > 0) { 257 | return PROCMAN_CMD_RUNNING; 258 | } 259 | if (cmd->Pid() == 0) { 260 | return PROCMAN_CMD_STOPPED; 261 | } 262 | return PROCMAN_CMD_INVALID; 263 | } 264 | 265 | void Procman::SetCommandExecStr(ProcmanCommandPtr cmd, 266 | const std::string& exec_str) { 267 | CheckCommand(cmd); 268 | cmd->exec_str_ = exec_str; 269 | } 270 | 271 | void Procman::CheckCommand(ProcmanCommandPtr cmd) { 272 | if (std::find(commands_.begin(), commands_.end(), cmd) == commands_.end()) { 273 | throw std::invalid_argument("invalid command"); 274 | } 275 | } 276 | 277 | } 278 | -------------------------------------------------------------------------------- /src/procman_ros/socket_monitor.cpp: -------------------------------------------------------------------------------- 1 | #include "procman_ros/socket_monitor.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | namespace procman { 15 | 16 | static int g_signal_fds[2] = { -1, -1 }; 17 | 18 | static int64_t Now() { 19 | struct timeval tv; 20 | gettimeofday (&tv, NULL); 21 | return (int64_t) tv.tv_sec * 1000000 + tv.tv_usec; 22 | } 23 | 24 | class SocketNotifier { 25 | public: 26 | ~SocketNotifier(); 27 | 28 | private: 29 | SocketNotifier(int fd, SocketMonitor::EventType event_type, 30 | std::function callback, 31 | SocketMonitor* loop); 32 | 33 | friend class SocketMonitor; 34 | 35 | int fd_; 36 | SocketMonitor::EventType event_type_; 37 | std::function callback_; 38 | std::weak_ptr weak_; 39 | SocketMonitor* loop_; 40 | }; 41 | 42 | SocketNotifier::SocketNotifier(int fd, SocketMonitor::EventType event_type, 43 | std::function callback, 44 | SocketMonitor* loop) : 45 | fd_(fd), 46 | event_type_(event_type), 47 | callback_(callback), 48 | loop_(loop) { 49 | } 50 | 51 | SocketNotifier::~SocketNotifier() { 52 | ROS_DEBUG("Destroying socket notifier %p for %d\n", this, fd_); 53 | 54 | auto iter = std::find(loop_->sockets_.begin(), loop_->sockets_.end(), this); 55 | if (iter != loop_->sockets_.end()) { 56 | ROS_DEBUG("found in sockets_\n"); 57 | loop_->sockets_.erase(iter); 58 | } 59 | 60 | // If the SocketNotifier being destroyed is a socket queued up for callback, 61 | // then zero out its place in the queue, but don't remove it to avoid messing 62 | // with queue iteration. 63 | auto ready_iter = std::find(loop_->sockets_ready_.begin(), 64 | loop_->sockets_ready_.end(), this); 65 | if (iter != loop_->sockets_ready_.end()) { 66 | *ready_iter = nullptr; 67 | } 68 | fd_ = -1; 69 | } 70 | 71 | SocketMonitor::SocketMonitor() : 72 | quit_(false) {} 73 | 74 | SocketMonitor::~SocketMonitor() { 75 | if (g_signal_fds[0] != -1) { 76 | close(g_signal_fds[0]); 77 | close(g_signal_fds[1]); 78 | g_signal_fds[0] = -1; 79 | g_signal_fds[1] = -1; 80 | } 81 | } 82 | 83 | SocketNotifierPtr SocketMonitor::AddSocket(int fd, 84 | EventType event_type, std::function callback) { 85 | if (event_type != kRead && 86 | event_type != kWrite && 87 | event_type != kError) { 88 | throw std::invalid_argument("Invalid socket event type"); 89 | } 90 | SocketNotifier* notifier = new SocketNotifier(fd, event_type, callback, this); 91 | sockets_.push_back(notifier); 92 | return SocketNotifierPtr(notifier); 93 | } 94 | 95 | static void signal_handler (int signum) { 96 | int wstatus = write(g_signal_fds[1], &signum, sizeof(int)); 97 | (void) wstatus; 98 | } 99 | 100 | void SocketMonitor::SetPosixSignals(const std::vector& signums, 101 | std::function callback) { 102 | if (g_signal_fds[0] != -1) { 103 | throw std::runtime_error("SocketMonitor POSIX signals already set"); 104 | } 105 | 106 | if (0 != pipe(g_signal_fds)) { 107 | throw std::runtime_error("Error initializing internal pipe for POSIX signals"); 108 | } 109 | 110 | const int flags = fcntl(g_signal_fds[1], F_GETFL); 111 | fcntl(g_signal_fds[1], F_SETFL, flags | O_NONBLOCK); 112 | 113 | for (int signum : signums) { 114 | signal(signum, signal_handler); 115 | } 116 | 117 | posix_signal_notifier_ = AddSocket(g_signal_fds[0], kRead, 118 | [this, callback]() { 119 | int signum; 120 | const int unused = read(g_signal_fds[0], &signum, sizeof(int)); 121 | (void) unused; 122 | callback(signum); 123 | }); 124 | } 125 | 126 | void SocketMonitor::IterateOnce() { 127 | // Prepare pollfd structure 128 | const int num_sockets = sockets_.size(); 129 | struct pollfd* pfds = new struct pollfd[num_sockets]; 130 | for (int index = 0; index < num_sockets; ++index) { 131 | pfds[index].fd = sockets_[index]->fd_; 132 | switch (sockets_[index]->event_type_) { 133 | case kRead: 134 | pfds[index].events = POLLIN; 135 | break; 136 | case kWrite: 137 | pfds[index].events = POLLOUT; 138 | break; 139 | case kError: 140 | pfds[index].events = POLLERR; 141 | break; 142 | default: 143 | pfds[index].events = POLLIN; 144 | break; 145 | } 146 | pfds[index].revents = 0; 147 | } 148 | 149 | // poll sockets for the maximum wait time. 150 | const int num_sockets_ready = poll(pfds, num_sockets, 300); 151 | 152 | // Check which sockets are ready, and queue them up for invoking callbacks. 153 | if (num_sockets_ready) { 154 | for (int index = 0; index < num_sockets; ++index) { 155 | struct pollfd* pfd = &pfds[index]; 156 | if (pfd->revents & pfd->events) { 157 | ROS_DEBUG("marking socket notifier %p (%d) for callback", 158 | sockets_[index], pfd->fd); 159 | sockets_ready_.push_back(sockets_[index]); 160 | } 161 | } 162 | } 163 | // Call callbacks for sockets that are ready 164 | for (int index = 0; index < sockets_ready_.size(); ++index) { 165 | SocketNotifier* notifier = sockets_ready_[index]; 166 | if (!notifier) { 167 | continue; 168 | } 169 | notifier->callback_(); 170 | } 171 | sockets_ready_.clear(); 172 | 173 | delete[] pfds; 174 | 175 | } 176 | 177 | } // namespace procman 178 | -------------------------------------------------------------------------------- /test/test.cfg: -------------------------------------------------------------------------------- 1 | group "args_test" { 2 | cmd "quote-args" { 3 | host = "localhost"; 4 | exec = "bash -c 'echo arg0: [$0] arg1: \"$1\" arg2: [$2]' abc \"def ghi\" 'jkl mno pqr'"; 5 | } 6 | } 7 | cmd "env_var_test" { 8 | host = "localhost"; 9 | exec = "ABC=def GHI=\"jkl mno\" bash -c 'echo abc: [$ABC] ghi: [${GHI}]'"; 10 | } 11 | cmd "lcm-spy" { 12 | host = "localhost"; 13 | exec = "lcm-spy"; 14 | } 15 | 16 | script "start-all" { 17 | start cmd "lcm-spy" wait "running"; 18 | start cmd "env_var_test" wait "running"; 19 | start cmd "quote-args" wait "running"; 20 | } 21 | 22 | script "start-all-until-stopped" { 23 | start cmd "quote-args" wait "stopped"; 24 | start cmd "env_var_test" wait "stopped"; 25 | start cmd "lcm-spy" wait "stopped"; 26 | } 27 | --------------------------------------------------------------------------------