├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── TODO ├── config ├── docs ├── ash-query.png ├── ash-rcwd.png ├── ash-session.png ├── diagram.png └── diagram.txt ├── man ├── _ash_log.1 └── ash_query.1 ├── python ├── .gitignore ├── Makefile ├── README ├── TODO ├── _ash_log.py ├── advanced_shell_history │ ├── __init__.py │ ├── unix.py │ └── util.py └── ash_query.py ├── queries ├── shell ├── bash ├── common └── zsh └── src ├── .gitignore ├── Makefile ├── README ├── TODO ├── _ash_log.cpp ├── _ash_log.hpp ├── ash_query.cpp ├── ash_query.hpp ├── command.cpp ├── command.hpp ├── config.cpp ├── config.hpp ├── database.cpp ├── database.hpp ├── flags.cpp ├── flags.hpp ├── formatter.cpp ├── formatter.hpp ├── logger.cpp ├── logger.hpp ├── queries.cpp ├── queries.hpp ├── queries.l ├── session.cpp ├── session.hpp ├── sqlite3.c ├── sqlite3.h ├── unix.cpp ├── unix.hpp ├── util.cpp └── util.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | /files/ 2 | /overlay.tar.gz 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017 Carl Anderson 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | REV := r2 18 | VERSION := 0.8 19 | UPDATED := 2018-01-03 20 | RVERSION := ${VERSION}${REV} 21 | ETC_DIR := /usr/local/etc/advanced-shell-history 22 | LIB_DIR := /usr/local/lib/advanced_shell_history 23 | BIN_DIR := /usr/local/bin 24 | TMP_ROOT := /tmp 25 | TMP_DIR := ${TMP_ROOT}/ash-${VERSION} 26 | TMP_FILE := ${TMP_DIR}.tar.gz 27 | MAN_DIR := /usr/share/man/man1 28 | SRC_DEST := .. 29 | SHELL := /bin/bash 30 | 31 | BEGIN_URL := https://github.com/barabo/advanced-shell-history 32 | 33 | .PHONY: all build build_c build_python clean fixperms install install_c install_python man mrproper src_tarball src_tarball_minimal uninstall 34 | all: build man 35 | 36 | new: clean all 37 | 38 | filesystem: man 39 | mkdir -p files/${BIN_DIR} 40 | mkdir -p files/${ETC_DIR} 41 | mkdir -p files/${LIB_DIR}/sh 42 | chmod 755 files/${LIB_DIR}/sh files/${ETC_DIR} 43 | cp shell/* files/${LIB_DIR}/sh 44 | cp config queries files/${ETC_DIR} 45 | 46 | build_python: filesystem 47 | @ printf "\nCompiling source code...\n" 48 | @ cd python && make VERSION="${RVERSION}" 49 | find python -type f -name '*.py' | xargs chmod 555 50 | cp -af python/*.py files/${BIN_DIR} 51 | cp -af python/advanced_shell_history/*.py files/${LIB_DIR} 52 | find python -type f -name '*.py' | xargs chmod 775 53 | 54 | build_c: filesystem 55 | @ printf "\nCompiling source code...\n" 56 | @ cd src && make VERSION="${RVERSION}" 57 | chmod 555 src/{_ash_log,ash_query} 58 | cp -af src/{_ash_log,ash_query} files/${BIN_DIR} 59 | 60 | build: build_python build_c 61 | 62 | man: 63 | @ printf "\nGenerating man pages...\n" 64 | mkdir -p files/${MAN_DIR} 65 | sed -e "s:__VERSION__:Version ${RVERSION}:" man/_ash_log.1 \ 66 | | sed -e "s:__DATE__:${UPDATED}:" \ 67 | | gzip -9 -c > ./files${MAN_DIR}/_ash_log.1.gz 68 | sed -e "s:__VERSION__:Version ${RVERSION}:" man/ash_query.1 \ 69 | | sed -e "s:__DATE__:${UPDATED}:" \ 70 | | gzip -9 -c > ./files${MAN_DIR}/ash_query.1.gz 71 | cp -af ./files${MAN_DIR}/_ash_log.1.gz ./files${MAN_DIR}/_ash_log.py.1.gz 72 | cp -af ./files${MAN_DIR}/ash_query.1.gz ./files${MAN_DIR}/ash_query.py.1.gz 73 | chmod 644 ./files${MAN_DIR}/*ash*.1.gz 74 | 75 | fixperms: filesystem 76 | chmod 644 files/${LIB_DIR}/* files/${ETC_DIR}/* 77 | chmod 755 files/${LIB_DIR}/sh 78 | 79 | overlay.tar.gz: fixperms 80 | @ cd files && sleep 10 && \ 81 | sudo tar -cvpzf ../overlay.tar.gz $$( \ 82 | find . -type f -o -type l \ 83 | | grep -v '\.git' \ 84 | ) 85 | 86 | install: build overlay.tar.gz uninstall 87 | @ echo "\nInstalling files:" 88 | sudo tar -xpv --no-same-owner -C / -f overlay.tar.gz 89 | @ printf "\n 0/ - Install completed!\nOS: User login. 5 | OS->bash: bash startup sequence. 6 | note over bash 7 | Sources ~/.bashrc 8 | Sources /usr/local/lib/advanced_shell_history/sh/bash 9 | Sets PROMPT_COMMAND=_ash_log 10 | end note 11 | bash->You: Wait for user. 12 | loop Interactive Shell. 13 | 14 | note over You: Type a command. 15 | You->bash: Press Enter 16 | note over bash 17 | Parses command. 18 | Command start time is logged. 19 | end note 20 | bash->OS: Execute command. 21 | note over OS: Command runs. 22 | OS->bash: Command exits. 23 | note over bash: Env Variables are set: $?, $PIPESTATUS 24 | 25 | bash->ASH: PROMPT_COMMAND executed. 26 | note over ASH 27 | Command end time is noted. 28 | PIPEST_ASH is updated. 29 | end note 30 | 31 | alt If PROMPT_COMMAND was defined before ASH 32 | ASH->bash: Original PROMPT_COMMAND executed. 33 | bash->OS: Execute. 34 | OS->bash: Exit. 35 | bash->ASH: Sets $?, $PIPESTATUS 36 | end 37 | 38 | ASH->bash: _ash_log invoked. 39 | bash->OS: _ash_log executes. 40 | note over OS: Command is saved to history.db. 41 | OS->bash: Original $? is restored. 42 | bash->You: New prompt is displayed. 43 | end 44 | -------------------------------------------------------------------------------- /man/_ash_log.1: -------------------------------------------------------------------------------- 1 | .\" 2 | .\"Copyright 2016 Carl Anderson 3 | .\" 4 | .\"Licensed under the Apache License, Version 2.0 (the "License"); 5 | .\"you may not use this file except in compliance with the License. 6 | .\"You may obtain a copy of the License at 7 | .\" 8 | .\" http://www.apache.org/licenses/LICENSE-2.0 9 | .\" 10 | .\"Unless required by applicable law or agreed to in writing, software 11 | .\"distributed under the License is distributed on an "AS IS" BASIS, 12 | .\"WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | .\"See the License for the specific language governing permissions and 14 | .\"limitations under the License. 15 | .\" 16 | 17 | .TH _ash_log 1 \ 18 | "Updated: __DATE__" \ 19 | "__VERSION__" \ 20 | "Advanced Shell History" 21 | 22 | 23 | .SH NAME 24 | _ash_log - The advanced shell history command logger. 25 | 26 | 27 | .SH SYNOPSIS 28 | Usage: _ash_log [options] 29 | -h --help 30 | -a --alert VALUE 31 | -c --command VALUE 32 | -e --command_exit VALUE 33 | -p --command_pipe_status VALUE 34 | -s --command_start VALUE 35 | -f --command_finish VALUE 36 | -n --command_number VALUE 37 | -x --exit VALUE 38 | -V --version 39 | -S --get_session_id 40 | -E --end_session 41 | 42 | 43 | .SH DESCRIPTION 44 | .B _ash_log and _ash_log.py are not intended to be executed manually. 45 | They are invoked automatically by the advanced shell history system which is 46 | only activated by sourcing the appropriate file (depending on whether you are 47 | using bash or zsh). 48 | 49 | Advanced Shell History includes: 50 | .RS 51 | Session details: 52 | .RS 53 | .IP "* login and logout times" 54 | .IP "* username and hostname" 55 | .IP "* incoming ssh connection details (ip and ports)" 56 | .IP "* shell name" 57 | .IP "* tty, shell pid and ppid" 58 | .RE 59 | 60 | Command details: 61 | .RS 62 | .IP "* start and stop times" 63 | .IP "* exit code and pipestatus codes" 64 | .IP "* current working directory" 65 | .IP "* session id" 66 | .RE 67 | 68 | Many more data points are also collected. 69 | .RE 70 | 71 | 72 | If you intend to use this system to manage your own personal command history, 73 | it is recommended that you add the appropriate source command to your shell rc 74 | file so it is automatically started with each new session. 75 | 76 | .B For bash, add this to your ~/.bashrc file: 77 | .RS 78 | source /usr/local/lib/advanced_shell_history/sh/bash 79 | .RE 80 | 81 | .B For zsh, add this to your ~/.zshrc file: 82 | .RS 83 | source /usr/local/lib/advanced_shell_history/sh/zsh 84 | .RE 85 | 86 | 87 | .SH OPTIONS 88 | .IP " -h --help" 89 | 90 | Display command help and exit 0. 91 | 92 | .IP " -a --alert VALUE" 93 | 94 | Display an alert message (VALUE). 95 | 96 | .IP " -c --command VALUE" 97 | 98 | Store the command (VALUE) entered by the user. 99 | 100 | .IP " -e --command_exit VALUE" 101 | 102 | Store the command exit code (VALUE) of the command. 103 | 104 | .IP " -p --command_pipe_status VALUE" 105 | 106 | Store the command pipe states (exit code for each piped command) separated by 107 | underscores. 108 | 109 | .IP " -s --command_start VALUE" 110 | 111 | The unix epoch timestamp (VALUE) when the command started (the user pressed 112 | Enter). 113 | 114 | .IP " -f --command_finish VALUE" 115 | 116 | The unix epoch timestamp (VALUE) when the command completed and the next prompt 117 | was displayed. 118 | 119 | .IP " -n --command_number VALUE" 120 | 121 | The shell builtin history number (VALUE) of the entered command. 122 | 123 | .IP " -x --exit VALUE" 124 | 125 | The exit code to use when exiting this program. 126 | This flag is needed to re-create the same exit status as the logged command. 127 | This is important because this command runs automatically before the prompt is 128 | redrawn, so if _ash_log does not restore the previous exit code, the user would 129 | see the exit code for _ash_log (and NOT their command) when executing: 130 | .RS 131 | echo ${?} 132 | .RE 133 | 134 | .IP " -V --version" 135 | 136 | Display the version number and exit. 137 | 138 | .IP " -S --get_session_id" 139 | 140 | Display the advanced shell history session number and exit. If no session ID 141 | is found, one is created and displayed. 142 | 143 | .IP " -E --end_session" 144 | 145 | Ends the current session, as defined by the shell environment variable 146 | ASH_SESSION_ID. It is an error to use this flag without having the 147 | ASH_SESSION_ID variable set. 148 | 149 | 150 | .SH FILES 151 | .I /etc/ash/ash.conf 152 | .RS 153 | Contains environment variables to be sourced into your shell. 154 | .RE 155 | 156 | .I /etc/ash/queries 157 | .RS 158 | Contains saved queries which can be invoked using the 159 | .BR ash_query(1) 160 | command. Also see 161 | .I ~/.ash/queries 162 | for user-written queries. 163 | .RE 164 | 165 | .I ~/.ash/history.db 166 | .RS 167 | The default location for the sqlite3 database holding command history. See 168 | https://github.com/barabo/advanced-shell-history/w for more details on how 169 | the data is stored internally and other tips for querying the data. 170 | .RE 171 | 172 | .I /usr/local/lib/advanced_shell_history/sh/bash 173 | .RS 174 | Sourced for bash sessions. 175 | .RE 176 | 177 | .I /usr/local/lib/advanced_shell_history/sh/zsh 178 | .RS 179 | Sourced for zsh sessions. 180 | .RE 181 | 182 | 183 | .SH ENVIRONMENT 184 | .IP ASH_CFG_DB_FAIL_RANDOM_TIMEOUT 185 | After a failed insert, sleep a random number of milliseconds before retrying. 186 | This is intended to add some noise to the retry mechanism. 187 | 188 | .IP ASH_CFG_DB_FAIL_TIMEOUT 189 | After a failed insert, sleep this many milliseconds before retrying. 190 | 191 | .IP ASH_CFG_DB_MAX_RETRIES 192 | Quit db retries after this many failed attempts. 193 | 194 | .IP ASH_CFG_HIDE_USAGE_FOR_NO_ARGS 195 | Normally, if you invoke ash_query with no arguments, the --help output is 196 | displayed. With this set to a non-empty value, the --help output is 197 | suppressed in this case. 198 | 199 | .IP ASH_CFG_HISTORY_DB 200 | The default database to query. This is set by sourcing one of the shell 201 | scripts in /usr/local/lib/advanced_shell_history/sh and signifies the location 202 | of the database where commands are logged. If this variable exists, the 203 | --database flag does not need to be used. 204 | 205 | .IP ASH_CFG_IGNORE_UNKNOWN_FLAGS 206 | Normally ash_query complains when it sees unknown flags. With this variable 207 | set to a non-empty value, unknown flags are ignored. 208 | 209 | .IP ASH_CFG_LOG_DATE_FMT 210 | If logging is in use, this format string can be set to customize the date 211 | string. 212 | 213 | .IP ASH_CFG_LOG_FILE 214 | The file destination of logged messages, if logging is in use. 215 | 216 | .IP ASH_CFG_LOG_IPV4 217 | Can be used to skip logging ipv4 host IP addresses. 218 | 219 | .IP ASH_CFG_LOG_IPV6 220 | Can be used to skip logging ipv6 host IP addresses. 221 | 222 | .IP ASH_CFG_LOG_LEVEL 223 | The lowest level of logging to make visible. Levels (in increasing order) 224 | are DEBUG, INFO, WARN, ERROR and FATAL. 225 | 226 | .IP ASH_CFG_SKIP_LOOPBACK 227 | Skip logging IP addresses for loopback devices (both ipv4 and ipv6). 228 | 229 | .IP ASH_DISABLED 230 | If set, _ash_log is disabled. 231 | 232 | .IP ASH_LOG_BIN 233 | The binary used to log command history to the database. This should be either 234 | _ash_log or _ash_log.py, depending on your system setup. 235 | 236 | .IP ASH_SESSION_ID 237 | The session id number created for the current session. If unset, a new 238 | session ID will be created and displayed. The caller is expected to export 239 | this variable using the generated ID number. 240 | 241 | 242 | .SH "SEE ALSO" 243 | .BR ash_query(1) 244 | to query history 245 | 246 | 247 | .SH AUTHOR 248 | Carl Anderson, Health Catalyst, Inc. 249 | 250 | 251 | .SH BUGS 252 | Report bugs at https://github.com/barabo/advanced-shell-history/issues 253 | -------------------------------------------------------------------------------- /man/ash_query.1: -------------------------------------------------------------------------------- 1 | .\" 2 | .\"Copyright 2016 Carl Anderson 3 | .\" 4 | .\"Licensed under the Apache License, Version 2.0 (the "License"); 5 | .\"you may not use this file except in compliance with the License. 6 | .\"You may obtain a copy of the License at 7 | .\" 8 | .\" http://www.apache.org/licenses/LICENSE-2.0 9 | .\" 10 | .\"Unless required by applicable law or agreed to in writing, software 11 | .\"distributed under the License is distributed on an "AS IS" BASIS, 12 | .\"WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | .\"See the License for the specific language governing permissions and 14 | .\"limitations under the License. 15 | .\" 16 | 17 | .TH ash_query 1 \ 18 | "Updated: __DATE__" \ 19 | "__VERSION__" \ 20 | "Advanced Shell History" 21 | 22 | 23 | .SH NAME 24 | ash_query - The advanced shell history query manager. 25 | 26 | 27 | .SH SYNOPSIS 28 | Usage: ash_query [options] 29 | --help 30 | -d --database VALUE 31 | -f --format VALUE 32 | -l --limit VALUE 33 | -p --print_query VALUE 34 | -q --query VALUE 35 | -F --list_formats 36 | -H --hide_headings 37 | -Q --list_queries 38 | --version 39 | 40 | 41 | .SH DESCRIPTION 42 | .B ash_query 43 | is a part of the Advanced Shell History package. It provides a 44 | convenient way to execute saved queries against a command history database. 45 | 46 | To collect command history data and to populate a history database, you must 47 | first source the appropriate shell code into your shell session. 48 | 49 | If you intend to use this system to manage your own personal command history, 50 | it is recommended that you add the appropriate source command to your shell rc 51 | file so it is automatically started with each new session. 52 | 53 | .B For bash, add this to your ~/.bashrc file: 54 | .RS 55 | source /usr/local/lib/advanced_shell_history/sh/bash 56 | .RE 57 | 58 | .B For zsh, add this to your ~/.zshrc file: 59 | .RS 60 | source /usr/local/lib/advanced_shell_history/sh/zsh 61 | .RE 62 | 63 | 64 | .SH OPTIONS 65 | .IP " --help" 66 | 67 | Display help and exit 0. 68 | 69 | .IP " -d --database VALUE" 70 | 71 | The filename (VALUE) of the database to query. 72 | Typically this will be ~/.ash/history.db. 73 | If this is not specified on the command line, ash_query will look for a shell 74 | environment variable ASH_CFG_HISTORY_DB and try to use that. 75 | 76 | .IP " -f --format VALUE" 77 | 78 | Select an output format (VALUE) from: 79 | 80 | .B aligned 81 | Columns are aligned and separated with spaces. 82 | 83 | .B csv 84 | Columns are comma separated with strings quoted. 85 | 86 | .B group 87 | Repeated values are summarized before the rows. 88 | 89 | .B null 90 | Columns are null separated with strings quoted. 91 | 92 | If format is not specified, ash_query will look for a default format 93 | environment variable ASH_CFG_DEFAULT_FORMAT and try to use that. 94 | If neither are specified, the default is 'aligned'. 95 | 96 | 97 | .IP " -l --limit VALUE" 98 | 99 | Return no more than VALUE rows. If the query already contains a limit 100 | clause, that clause overrides this value. This value is ignored if less 101 | than or equal to zero. 102 | 103 | .IP " -p --print_query VALUE" 104 | 105 | Print the named query (VALUE) to stdout. 106 | If the query uses shell variables, the generic query will be printed in 107 | addition to the query after variable substitution. 108 | 109 | .IP " -q --query VALUE" 110 | 111 | Execute the named query (VALUE) and display results formatted according to 112 | the specified --format. If no query is specified, ash_query will check for 113 | a named query in the shell environment variable ASH_CFG_DEFAULT_QUERY and 114 | attempt to use that. 115 | 116 | .IP " -F --list_formats" 117 | 118 | List all the available output formats. 119 | 120 | .IP " -H --hide_headings" 121 | 122 | Suppress the headings of output tables (sometimes useful for scripting). 123 | 124 | .IP " -Q --list_queries" 125 | 126 | List the names and descriptions of all available saved queries taken from 127 | /etc/ash/queries and ~/.ash/queries. 128 | 129 | .IP " --version" 130 | 131 | Display the version number and exit. 132 | 133 | 134 | .SH FILES 135 | .I /etc/ash/ash.conf 136 | .RS 137 | Contains environment variables to be sourced into your shell. 138 | .RE 139 | 140 | .I /etc/ash/queries 141 | .RS 142 | Contains saved queries which can be invoked using the 143 | .BR ash_query(1) 144 | command. Also see 145 | .I ~/.ash/queries 146 | for user-written queries. 147 | .RE 148 | 149 | .I ~/.ash/history.db 150 | .RS 151 | The default location for the sqlite3 database holding command history. See 152 | https://github.com/barabo/advanced-shell-history/w for more details on how 153 | the data is stored internally and other tips for querying the data. 154 | .RE 155 | 156 | .I /usr/local/lib/advanced_shell_history/sh/bash 157 | .RS 158 | Sourced for bash sessions. 159 | .RE 160 | 161 | .I /usr/local/lib/advanced_shell_history/sh/zsh 162 | .RS 163 | Sourced for zsh sessions. 164 | .RE 165 | 166 | 167 | .SH ENVIRONMENT 168 | .IP ASH_CFG_DB_FAIL_RANDOM_TIMEOUT 169 | After a failed select, sleep a random number of milliseconds before retrying. 170 | This is intended to add some noise to the retry mechanism. 171 | 172 | .IP ASH_CFG_DB_FAIL_TIMEOUT 173 | After a failed select, sleep this many milliseconds before retrying. 174 | 175 | .IP ASH_CFG_DB_MAX_RETRIES 176 | Quit db retries after this many failed attempts. 177 | 178 | .IP ASH_CFG_DEFAULT_FORMAT 179 | The default format to display queried data returned by ash_query. Set this 180 | to something other than 'aligned' if you prefer a different format. 181 | 182 | .IP ASH_CFG_DEFAULT_QUERY 183 | The default query to execute by ash_query. Set this to the name of your 184 | favorite query if you don't want to specify the same query name each time. 185 | 186 | .IP ASH_CFG_HIDE_USAGE_FOR_NO_ARGS 187 | Normally, if you invoke ash_query with no arguments, the --help output is 188 | displayed. With this set to a non-empty value, the --help output is 189 | suppressed in this case. 190 | 191 | .IP ASH_CFG_HISTORY_DB 192 | The default database to query. This is set by sourcing one of the shell 193 | scripts in /usr/local/lib/advanced_shell_history/sh and signifies the location 194 | of the database where commands are logged. If this variable exists, the 195 | --database flag does not need to be used. 196 | 197 | .IP ASH_CFG_IGNORE_UNKNOWN_FLAGS 198 | Normally ash_query complains when it sees unknown flags. With this variable 199 | set to a non-empty value, unknown flags are ignored. 200 | 201 | .IP ASH_CFG_LOG_DATE_FMT 202 | If logging is in use, this format string can be set to customize the date 203 | string. 204 | 205 | .IP ASH_CFG_LOG_FILE 206 | The file destination of logged messages, if logging is in use. 207 | 208 | .IP ASH_CFG_LOG_LEVEL 209 | The lowest level of logging to make visible. Levels (in increasing order) 210 | are DEBUG, INFO, WARN, ERROR and FATAL. 211 | 212 | 213 | .SH "SEE ALSO" 214 | .BR _ash_log(1) 215 | for logging history 216 | 217 | 218 | .SH AUTHOR 219 | Carl Anderson, Health Catalyst, Inc. 220 | 221 | 222 | .SH BUGS 223 | Report bugs at https://github.com/barabo/advanced-shell-history/issues 224 | -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | # These files are created in OSX when using sed -i -e to inject versions. 2 | *.py-e 3 | -------------------------------------------------------------------------------- /python/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017 Carl Anderson 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # The VERSION variable is passed to this makefile from the main Makefile. 18 | VERSION := placeholder 19 | VERSIONED := _ash_log.py advanced_shell_history/*.py ash_query.py 20 | 21 | .default: version 22 | 23 | version: 24 | sed -i -e "s:^__version__ = .*:__version__ = '${VERSION}':" ${VERSIONED} 25 | 26 | clean: 27 | find . -type f -name '*.pyc' | xargs rm -f 28 | find . -type f -name '*.py-e' | xargs rm -f 29 | -------------------------------------------------------------------------------- /python/README: -------------------------------------------------------------------------------- 1 | This is a Python rewrite of the C++ code. 2 | 3 | Partly because it's easier: 4 | - there is a native Python flag parsing library in 2.7+ (argparse) 5 | - Python comes integrated with sqlite3 6 | - Python comes with a logging library. 7 | - The python array operations make lots of parts of the code simpler. 8 | 9 | I wrote the initial version in C++ for performance reasons, but am finding that 10 | there are unforseen compilation issues with it. Some architectures do not 11 | support the realtime clock that I'm using and there is a new version of the STL 12 | library that segfaults when I try to use it. 13 | 14 | Also, on most systems, calling Python once per command is not a terrible price 15 | to pay. On my system, python2.7 is a 3M binary. It lives in memory as long as 16 | you are entering commands, so you really only pay to load it the first time. 17 | -------------------------------------------------------------------------------- /python/TODO: -------------------------------------------------------------------------------- 1 | TODO 2 | [ ] determine the best way to install a local python library to the system path, so sys.path doesn't need to be patched. 3 | [ ] for OSX install, put files into /Applications/Advanced Shell History/Contents/Resources/python/advanced_shell_history 4 | [ ] virtual environment support would be nice 5 | [ ] profile for performance - the c++ version is 10x faster 6 | [ ] 7 | [ ] database class 8 | (x) - open or create the database defined in the environment 9 | ( ) - time the DB writes and timeout if over the threshold 10 | ( ) - 11 | -------------------------------------------------------------------------------- /python/_ash_log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2018 Carl Anderson 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """A module to log commands and shell session details into a sqlite3 database. 18 | 19 | This module logs a command into a command history database, while also gathering 20 | system-specific metadata. 21 | """ 22 | from __future__ import print_function 23 | 24 | __author__ = 'Carl Anderson (carl.anderson@gmail.com)' 25 | __version__ = '0.8r2' 26 | 27 | 28 | import logging 29 | import os 30 | import sys 31 | 32 | # Allow the local advanced_shell_history library to be imported. 33 | _LIB = '/usr/local/lib' 34 | if _LIB not in sys.path: 35 | sys.path.append(_LIB) 36 | 37 | from advanced_shell_history import unix 38 | from advanced_shell_history import util 39 | 40 | 41 | class Flags(util.Flags): 42 | """The flags needed for the _ash_log.py script to work.""" 43 | 44 | arguments = ( 45 | ('a', 'alert', 'MSG', str, 'a message to display to the user'), 46 | ('c', 'command', 'CMD', str, 'a command to log'), 47 | ('e', 'command_exit', 'CODE', int, 'the exit code of the command to log'), 48 | ('p', 'command_pipe_status', 'CSV', str, 'the pipe states of the command to log'), 49 | ('s', 'command_start', 'TS', int, 'the timestamp when the command started'), 50 | ('f', 'command_finish', 'TS', int, 'the timestamp when the command stopped'), 51 | ('n', 'command_number', 'NUM', int, 'the builtin shell history command number'), 52 | ('x', 'exit', 'CODE', int, 'the exit code to use when exiting'), 53 | ) 54 | 55 | flags = ( 56 | ('S', 'get_session_id', 'emits the session ID (or creates one)'), 57 | ('E', 'end_session', 'ends the current session'), 58 | ) 59 | 60 | def __init__(self): 61 | util.Flags.__init__(self, Flags.arguments, Flags.flags) 62 | 63 | 64 | class Session(util.Database.Object): 65 | """An abstraction of a shell session to store to the history database.""" 66 | 67 | def __init__(self): 68 | """Initialize a Session, populating session values.""" 69 | util.Database.Object.__init__(self, 'sessions') 70 | self.values = { 71 | 'time_zone': unix.GetTimeZone(), 72 | 'start_time': unix.GetTime(), 73 | 'ppid': unix.GetPPID(), 74 | 'pid': unix.GetPID(), 75 | 'tty': unix.GetTTY(), 76 | 'uid': unix.GetUID(), 77 | 'euid': unix.GetEUID(), 78 | 'logname': unix.GetLoginName(), 79 | 'hostname': unix.GetHostName(), 80 | 'host_ip': unix.GetHostIp(), 81 | 'shell': unix.GetShell(), 82 | 'sudo_user': unix.GetEnv('SUDO_USER'), 83 | 'sudo_uid': unix.GetEnv('SUDO_UID'), 84 | 'ssh_client': unix.GetEnv('SSH_CLIENT'), 85 | 'ssh_connection': unix.GetEnv('SSH_CONNECTION') 86 | } 87 | 88 | def GetCreateTableSql(self): 89 | return ''' 90 | CREATE TABLE sessions ( 91 | id integer primary key autoincrement, 92 | hostname varchar(128), 93 | host_ip varchar(40), 94 | ppid int(5) not null, 95 | pid int(5) not null, 96 | time_zone str(3) not null, 97 | start_time integer not null, 98 | end_time integer, 99 | duration integer, 100 | tty varchar(20) not null, 101 | uid int(16) not null, 102 | euid int(16) not null, 103 | logname varchar(48), 104 | shell varchar(50) not null, 105 | sudo_user varchar(48), 106 | sudo_uid int(16), 107 | ssh_client varchar(60), 108 | ssh_connection varchar(100) 109 | )''' 110 | 111 | def Close(self): 112 | """Closes this session in the database.""" 113 | sql = ''' 114 | UPDATE sessions 115 | SET 116 | end_time = ?, 117 | duration = ? - start_time 118 | WHERE id == ?; 119 | ''' 120 | ts = unix.GetTime() 121 | util.Database().Execute(sql, (ts, ts, unix.GetEnvInt('ASH_SESSION_ID'),)) 122 | 123 | 124 | class Command(util.Database.Object): 125 | """An abstraction of a command to store to the history database.""" 126 | def __init__(self, command, rval, start, finish, number, pipes): 127 | util.Database.Object.__init__(self, 'commands') 128 | self.values = { 129 | 'session_id': unix.GetEnvInt('ASH_SESSION_ID'), 130 | 'shell_level': unix.GetEnvInt('SHLVL'), 131 | 'command_no': number, 132 | 'tty': unix.GetTTY(), 133 | 'euid': unix.GetEUID(), 134 | 'cwd': unix.GetCWD(), 135 | 'rval': rval, 136 | 'start_time': start, 137 | 'end_time': finish, 138 | 'duration': finish - start, 139 | 'pipe_cnt': len(pipes.split('_')), 140 | 'pipe_vals': pipes, 141 | 'command': command 142 | } 143 | # If the user changed directories, CWD will be the new directory, not the 144 | # one where the command was actually entered. 145 | if rval == 0 and (command == 'cd' or command.startswith('cd ')): 146 | self.values['cwd'] = unix.GetEnv('OLDPWD') 147 | 148 | def GetCreateTableSql(self): 149 | return ''' 150 | CREATE TABLE commands ( 151 | id integer primary key autoincrement, 152 | session_id integer not null, 153 | shell_level integer not null, 154 | command_no integer, 155 | tty varchar(20) not null, 156 | euid int(16) not null, 157 | cwd varchar(256) not null, 158 | rval int(5) not null, 159 | start_time integer not null, 160 | end_time integer not null, 161 | duration integer not null, 162 | pipe_cnt int(3), 163 | pipe_vals varchar(80), 164 | command varchar(1000) not null, 165 | UNIQUE(session_id, command_no) 166 | )''' 167 | 168 | 169 | def main(argv): 170 | # If ASH_DISABLED is set, we skip everything and exit without error. 171 | if os.getenv('ASH_DISABLED'): return 0 172 | 173 | # Setup. 174 | util.InitLogging() 175 | 176 | # Log the command, if debug logging is enabled. 177 | if logging.getLogger().isEnabledFor(logging.DEBUG): 178 | command = [] 179 | for arg in argv: 180 | command.append('[%d]=\'%s\'' % (len(command), arg)) 181 | logging.debug('argv = "' + ','.join(command) + '"') 182 | 183 | # Print an alert if one was specified. 184 | flags = Flags() 185 | if flags.alert: 186 | print(flags.alert, file=sys.stderr) 187 | 188 | # If no arguments were given, it may be best to show --help. 189 | if len(argv) == 1 and not util.Config().GetBool('HIDE_USAGE_FOR_NO_ARGS'): 190 | flags.PrintHelp() 191 | 192 | # Create the session id, if not already set in the environment. 193 | session_id = os.getenv('ASH_SESSION_ID') 194 | if flags.get_session_id: 195 | if session_id is None: 196 | session_id = Session().Insert() 197 | print(session_id) 198 | 199 | # Insert a new command into the database, if one was supplied. 200 | command_flag_used = bool(flags.command 201 | or flags.command_exit 202 | or flags.command_pipe_status 203 | or flags.command_start 204 | or flags.command_finish 205 | or flags.command_number) 206 | if command_flag_used: 207 | Command( 208 | flags.command, flags.command_exit, flags.command_start, 209 | flags.command_finish, flags.command_number, flags.command_pipe_status 210 | ).Insert() 211 | 212 | # End the current session. 213 | if flags.end_session: 214 | Session().Close() 215 | 216 | # Return the desired exit code. 217 | return flags.exit 218 | 219 | 220 | if __name__ == '__main__': 221 | sys.exit(main(sys.argv)) 222 | -------------------------------------------------------------------------------- /python/advanced_shell_history/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Carl Anderson 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /python/advanced_shell_history/unix.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Carl Anderson 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """A helper library to expose Unix system information.""" 17 | 18 | import os 19 | import pwd 20 | import re 21 | import socket 22 | import subprocess 23 | import sys 24 | import time 25 | 26 | 27 | class Error(Exception): 28 | pass 29 | 30 | 31 | def GetCWD(): 32 | """Returns the current working directory.""" 33 | return os.getcwd() 34 | 35 | 36 | def GetEnv(variable): 37 | """Returns the environment variable value as a string.""" 38 | return os.getenv(variable) 39 | 40 | 41 | def GetEnvInt(variable): 42 | """Returns the environment variable value as an integer.""" 43 | return int(os.getenv(variable) or 0) 44 | 45 | 46 | def GetEUID(): 47 | """Returns the current effective user ID as an int.""" 48 | return os.geteuid() 49 | 50 | 51 | def _GetIfconfig(): 52 | """Returns the lines emitted by /sbin/ifconfig -a""" 53 | try: 54 | fd = subprocess.Popen( 55 | "/sbin/ifconfig -a", 56 | shell=True, 57 | bufsize=8000, 58 | stdout=subprocess.PIPE).stdout 59 | return [x.lower().rstrip().decode('utf-8') for x in fd.readlines()] 60 | except Error: 61 | return None 62 | 63 | 64 | def _ParseIfconfig(): 65 | """Returns a dict of devices to ip addresses for this machine.""" 66 | device_matcher = re.compile(r'([^:\s]+)[:\s]') 67 | address_matcher = re.compile(r'\s+(inet6?\s)(addr:)?\s?([^\s/%]+)') 68 | mac_matcher = re.compile(r'.*\s(hwaddr|ether)\s([0-9a-f:]+)') 69 | 70 | device = None 71 | devices = {} 72 | inet_addresses = [] 73 | mac_address = '' 74 | 75 | for line in _GetIfconfig(): 76 | if device_matcher.match(line): 77 | if device: 78 | devices['%s|%s' % (device, mac_address)] = inet_addresses 79 | inet_addresses = [] 80 | mac_address = '' 81 | device = device_matcher.match(line).groups()[0] 82 | elif address_matcher.match(line): 83 | inet_addresses.append(address_matcher.match(line).groups()[2]) 84 | if mac_matcher.match(line): 85 | mac_address = mac_matcher.match(line).groups()[1] 86 | 87 | return devices 88 | 89 | 90 | def GetHostIp(): 91 | """Returns the ip addresses for this host.""" 92 | ips = [] 93 | # TODO(cpa): respect the relevant ASH_CFG_ settings here. 94 | for addresses in _ParseIfconfig().itervalues(): 95 | for address in addresses: 96 | ips.append(address) 97 | return ' '.join(ips) 98 | 99 | 100 | def GetHostName(): 101 | """Returns the hostname.""" 102 | return socket.gethostname() 103 | 104 | 105 | def GetLoginName(): 106 | """Returns the user login name.""" 107 | return pwd.getpwuid(os.getuid())[0] 108 | 109 | 110 | def GetPID(): 111 | """Returns the PID of the shell.""" 112 | return os.getppid() 113 | 114 | 115 | def GetPPID(): 116 | """Returns the PPID of the shell.""" 117 | return _GetProcStat(3) 118 | 119 | 120 | def _GetProcStat(num): 121 | """Returns the i'th field of /proc//stat of the shell.""" 122 | stat_file = '/proc/%d/stat' % os.getppid() 123 | if not os.path.exists(stat_file): 124 | return '' 125 | with open(stat_file) as fd: 126 | data = fd.read() 127 | return data.split(' ', num + 1)[num] 128 | 129 | 130 | def GetShell(): 131 | """Returns the name of the shell (ie: either zsh or bash)""" 132 | return _GetProcStat(1).strip('()') 133 | 134 | 135 | def GetTime(): 136 | """Returns the epoch timestamp.""" 137 | return long(time.time()) 138 | 139 | 140 | def GetTimeZone(): 141 | """Returns the local time zone string.""" 142 | return time.tzname[time.localtime()[8]] 143 | 144 | 145 | def GetTTY(): 146 | """Return the name of the current controlling tty.""" 147 | tty_name = os.ttyname(sys.stdin.fileno()) 148 | if tty_name and tty_name.startswith('/dev/'): 149 | return tty_name[5:] 150 | return tty_name 151 | 152 | 153 | def GetUID(): 154 | """Returns the UID of the command.""" 155 | return os.getuid() 156 | -------------------------------------------------------------------------------- /python/advanced_shell_history/util.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Carl Anderson 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """A library for general use by the advanced shell history utilities. 17 | 18 | This library provides components used by _ash_log.py and ash_query.py for 19 | logging, flag parsing, configuration and database management. 20 | """ 21 | 22 | __author__ = 'Carl Anderson (carl.anderson@gmail.com)' 23 | __version__ = '0.8r2' 24 | 25 | 26 | import argparse 27 | import logging 28 | import os 29 | import sqlite3 30 | import sys 31 | 32 | 33 | class Flags(argparse.ArgumentParser): 34 | """A class to manage all the flags for this advanced shell history utility.""" 35 | 36 | class Formatter(argparse.HelpFormatter): 37 | """A simple formatter whith a slightly wider set of flag names.""" 38 | def __init__(self, prog): 39 | argparse.HelpFormatter.__init__(self, prog, max_help_position=44) 40 | 41 | def __init__(self, arguments=None, flags=None): 42 | """Initialize the Flags.""" 43 | parser = argparse.ArgumentParser(formatter_class=Flags.Formatter) 44 | 45 | # Add the standard argument-taking flags. 46 | for short_flag, long_flag, metavar, arg_type, help_text in arguments or []: 47 | parser.add_argument('-' + short_flag, '--' + long_flag, metavar=metavar, 48 | type=arg_type, help=help_text) 49 | 50 | # Add the standard no-argument-taking flags. 51 | for short_flag, long_flag, help_text in flags or []: 52 | parser.add_argument('-' + short_flag, '--' + long_flag, 53 | action='store_true', help=help_text) 54 | 55 | # Add a flag to display the version and exit. 56 | parser.add_argument('-V', '--version', action='version', version=__version__, 57 | help='prints the version and exits') 58 | 59 | self._parser = parser 60 | self.flags = parser.parse_args().__dict__ 61 | self.__dict__.update(self.flags) 62 | 63 | def PrintHelp(self): 64 | """Prints the help menu.""" 65 | self._parser.print_help() 66 | 67 | 68 | class Config(object): 69 | """A class to manage the configuration environment variables. 70 | 71 | All environment variables beginning with the prefix 'ASH_CFG_' are loaded 72 | and made accessible conveniently through an instance of this class. 73 | 74 | For example: 75 | ASH_CFG_HISTORY_DB='/foo/' becomes { 'HISTORY_DB': '/foo/' } 76 | """ 77 | 78 | def __init__(self): 79 | """Initialize a Config instance, reading os.environ for variables.""" 80 | # Select all the environment variables starting with 'ASH_CFG_' and strip 81 | # off the leading ASH_CFG_ portion to use as the name of the variable. 82 | self.variables = dict( 83 | [(x[8:], y) for x, y in os.environ.items() if x.startswith('ASH_CFG_')] 84 | ) 85 | 86 | def GetBool(self, variable): 87 | """Returns a bool value for a config variable, or None if not set.""" 88 | value = self.GetString(variable) 89 | return value and value.strip() == 'true' 90 | 91 | def GetString(self, variable): 92 | """Returns a string value for a config variable, or None if not set.""" 93 | if self.Sets(variable): 94 | return self.variables[variable.upper().strip()] 95 | 96 | def Sets(self, variable): 97 | """Returns true when the argument variable exists in the environment.""" 98 | return variable and variable.upper().strip() in self.variables 99 | 100 | 101 | def InitLogging(): 102 | """Initializes the logging module. 103 | 104 | Uses the following shell environment variables to configure the logger: 105 | ASH_CFG_LOG_DATE_FMT - to format the date strings in the log file. 106 | ASH_CFG_LOG_LEVEL - to set the logging level (DEBUG, INFO, etc). 107 | ASH_CFG_LOG_FILE - the filename where the logger will write. 108 | ASH_SESSION_ID - the session id to include in the logged output. 109 | 110 | Lines are written in roughly this format: 111 | 2012-07-17 23:59:59 PDT: SESSION 123: DEBUG: argv = "[0]='ls'" 112 | """ 113 | session_id = os.getenv('ASH_SESSION_ID') or 'NEW' 114 | config = Config() 115 | level = config.GetString('LOG_LEVEL') or 'INFO' 116 | level = hasattr(logging, level) and getattr(logging, level) or logging.DEBUG 117 | fmt = '%(asctime)sSESSION ' + session_id + ': %(levelname)s: %(message)s' 118 | kwargs = { 119 | 'datefmt': config.GetString('LOG_DATE_FMT'), 120 | 'filename': config.GetString('LOG_FILE'), 121 | 'format': fmt, 122 | 'level': level, 123 | } 124 | logging.basicConfig(**kwargs) 125 | 126 | 127 | class Database(object): 128 | """A wrapper around a database connection.""" 129 | 130 | # The name of the sqlite3 file backing the saved command history. 131 | filename = None 132 | 133 | class Object(object): 134 | """A construct for objects to be inserted into the Database.""" 135 | def __init__(self, table_name): 136 | self.values = {} 137 | self.table_name = table_name 138 | sql = ''' 139 | select sql 140 | from sqlite_master 141 | where 142 | type = 'table' 143 | and name = ?; 144 | ''' 145 | # Check that the table exists, creating it if not. 146 | db = Database() 147 | cur = db.cursor 148 | try: 149 | cur.execute(sql, (table_name,)) 150 | rs = cur.fetchone() 151 | if not rs: 152 | cur.execute(self.GetCreateTableSql() + ';') 153 | db.connection.commit() 154 | elif rs[0] != self.GetCreateTableSql().strip(): 155 | logging.warning('Table %s exists, but has an unexpected schema.', 156 | table_name) 157 | finally: 158 | cur.close() 159 | 160 | def Insert(self): 161 | """Insert the object into the database, returning the new rowid.""" 162 | sql = 'INSERT INTO %s ( %s ) VALUES ( %s )' % ( 163 | self.table_name, 164 | ', '.join(self.values), 165 | ', '.join(['?' for _ in self.values]) 166 | ) 167 | return Database().Execute(sql, tuple(self.values.values())) 168 | 169 | def __init__(self): 170 | """Initialize a Database with an open connection to the history database.""" 171 | if Database.filename is None: 172 | Database.filename = Config().GetString('HISTORY_DB') 173 | if Database.filename is None: 174 | logging.error('Missing ASH_CFG_HISTORY_DB variable?') 175 | self.connection = sqlite3.connect(Database.filename) 176 | self.connection.row_factory = sqlite3.Row 177 | self.cursor = self.connection.cursor() 178 | 179 | def Execute(self, sql, values): 180 | try: 181 | self.cursor.execute(sql, values) 182 | logging.debug('executing query: %s, values = %r', sql, values) 183 | return self.cursor.lastrowid 184 | except sqlite3.IntegrityError as e: 185 | logging.debug('constraint violation: %r', e) 186 | finally: 187 | self.connection.commit() 188 | self.cursor.close() 189 | return 0 190 | 191 | @classmethod 192 | def SanityCheck(cls, sql): 193 | return sql and sqlite3.complete_statement(sql) 194 | 195 | def Fetch(self, sql, params=(), limit=None): 196 | """Execute a select query and return the result set.""" 197 | if self.SanityCheck(sql): 198 | try: 199 | self.cursor.execute(sql, params) 200 | row = self.cursor.fetchone() 201 | if not row: return None 202 | headings = tuple(row.keys()) 203 | fetched = 1 204 | if limit is None or limit <= 0: 205 | rows = self.cursor.fetchall() 206 | else: 207 | rows = [] 208 | while fetched < limit: 209 | row = self.cursor.fetchone() 210 | if not row: break 211 | rows.append(row) 212 | fetched += 1 213 | rows.insert(0, headings) 214 | rows.insert(1, row) 215 | return rows 216 | except sqlite3.Error as e: 217 | print >> sys.stderr, 'Failed to execute query: %s (%s)' % (sql, params) 218 | return None 219 | finally: 220 | self.cursor.close() 221 | self.cursor = None 222 | -------------------------------------------------------------------------------- /python/ash_query.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2018 Carl Anderson 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """A script to query command history from a sqlite3 database. 18 | 19 | This script fetches data from a command history database, using one of several 20 | user-defined queries. 21 | 22 | TOOD(cpa): add logging to this at some point. 23 | """ 24 | from __future__ import print_function 25 | 26 | __author__ = 'Carl Anderson (carl.anderson@gmail.com)' 27 | __version__ = '0.8r2' 28 | 29 | 30 | import csv 31 | import os 32 | import re 33 | import sys 34 | 35 | # Allow the local advanced_shell_history library to be imported. 36 | _LIB = '/usr/local/lib' 37 | if _LIB not in sys.path: 38 | sys.path.append(_LIB) 39 | 40 | from advanced_shell_history import util 41 | 42 | 43 | class Flags(util.Flags): 44 | """A class to manage all the flags for the command logger.""" 45 | 46 | arguments = ( 47 | ('d', 'database', 'DB', str, 'a history database to query'), 48 | ('f', 'format', 'FMT', str, 'a format to display results'), 49 | ('l', 'limit', 'LINES', int, 'a limit to the number of lines returned'), 50 | ('p', 'print_query', 'NAME', str, 'print the query SQL'), 51 | ('q', 'query', 'NAME', str, 'the name of the saved query to execute'), 52 | ) 53 | 54 | flags = ( 55 | ('F', 'list_formats', 'display all available formats'), 56 | ('H', 'hide_headings', 'hide column headings from query results'), 57 | ('Q', 'list_queries', 'display all saved queries'), 58 | ) 59 | 60 | def __init__(self): 61 | """Initialize the Flags.""" 62 | util.Flags.__init__(self, Flags.arguments, Flags.flags) 63 | 64 | 65 | class Queries(object): 66 | """A class to store all the queries available to ash_query.py. 67 | 68 | Queries are parsed from /usr/local/etc/advanced-shell-history/queries and 69 | ~/.ash/queries and are made available to the command line utility. 70 | 71 | TODO(cpa): if there is an error in the file, something should be printed. 72 | """ 73 | queries = [] 74 | show_headings = True 75 | parser = re.compile(r""" 76 | \s*(?P[A-Za-z0-9_-]+)\s*:\s*{\s* 77 | description\s*:\s* 78 | (?P 79 | "([^"]|\\")*" # A double-quoted string. 80 | )\s* 81 | sql\s*:\s*{ 82 | (?P 83 | ( 84 | [$]{[^}]*} | # Shell variable expressions: ${FOO} or ${BAR:-0} 85 | [^}] # Everything else in the query. 86 | )* 87 | ) 88 | }\s* 89 | }""", re.VERBOSE) 90 | 91 | @classmethod 92 | def Init(cls): 93 | if cls.queries: return 94 | 95 | # Load the queries from the system query file, and also the user file. 96 | data = [] 97 | system_queries = util.Config().GetString('SYSTEM_QUERY_FILE') 98 | user_queries = os.path.join(os.getenv('HOME'), '.ash', 'queries') 99 | for filename in (system_queries, user_queries): 100 | if not filename or not os.path.exists(filename): continue 101 | lines = [x for x in open(filename).readlines() if x and x[0] != '#'] 102 | data.extend([x[:-1] for x in lines if x[:-1]]) 103 | 104 | # Parse the loaded config files. 105 | cls.queries = {} # {name: (description, sql)} 106 | for match in cls.parser.finditer('\n'.join(data)): 107 | query_name = match.group('query_name') 108 | description = match.group('description') or '""' 109 | cls.queries[query_name] = (description[1:-1], match.group('sql')) 110 | 111 | @classmethod 112 | def Get(cls, query_name): 113 | if not query_name or not query_name in cls.queries: return (None, None) 114 | raw = cls.queries[query_name][1] 115 | sql = os.popen('/bin/cat </dev/null; then 119 | "${ASH_PROMPT_COMMAND}" 120 | fi 121 | # Causes the exit code to be reset to what it was before logging. 122 | local rval=${1:-0} && shift 123 | PIPEST_ASH=( ${@:-0} ) 124 | ${ASH_LOG_BIN} --exit ${rval} 125 | } 126 | 127 | 128 | ## 129 | # Invoked by __ash_log. 130 | # 131 | function __ash_last_command() { 132 | # Prevent users from manually invoking this function from the command line. 133 | [[ "${ASH:-0}" == "0" ]] && __ash_info __ash_last_command && return 134 | 135 | local cmd_no start_ts end_ts="$( date +%s )" cmd 136 | read -r cmd_no start_ts cmd <<< "$( builtin history 1 )" 137 | echo ${cmd_no:-0} ${start_ts:-0} ${end_ts:-0} "${cmd:-UNKNOWN}" 138 | } 139 | 140 | # This avoids logging duplicate commands when the user presses Ctrl-C while 141 | # entering a command. 142 | # Only need to trap Ctrl-C in bash. zsh do not need to do it and no duplicate 143 | # history in zsh. (Ctrl-C will stop works if do the same trap in zsh!) 144 | trap 'ASH_SKIP=1' INT 145 | 146 | # Protect the functions. 147 | readonly -f __ash_begin_session 148 | readonly -f __ash_last_command 149 | readonly -f __ash_precmd 150 | 151 | # Export functions used by subshells (not begin_session). 152 | #export -f __ash_last_command 153 | #export -f __ash_precmd 154 | -------------------------------------------------------------------------------- /shell/common: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2018 Carl Anderson 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # 18 | # This file is intended to be common shell code for both zsh and bash. 19 | # 20 | 21 | # Make sure there is a command logger. 22 | if [[ -z "${ASH_LOG_BIN}" ]]; then 23 | ASH_LOG_BIN=/usr/local/bin/_ash_log 24 | fi 25 | if ! [[ -x "${ASH_LOG_BIN}" ]]; then 26 | ASH_LOG_BIN=/usr/local/bin/_ash_log.py 27 | fi 28 | if ! [[ -x "${ASH_LOG_BIN}" ]]; then 29 | return 30 | fi 31 | 32 | 33 | # Create the directory holding the history database. 34 | if [[ ! -e "${ASH_CFG_HISTORY_DB}" ]]; then 35 | mkdir -p "$( dirname "${ASH_CFG_HISTORY_DB}" )" \ 36 | || ${ASH_LOG_BIN} -a "Failed to mkdir -p $( dirname "${ASH_CFG_HISTORY_DB}" )" 37 | fi 38 | 39 | 40 | # Ensure there is a HISTFILE. 41 | if [[ -z "${HISTFILE}" ]]; then 42 | export HISTFILE="${ASH_CFG_HISTORY_DB%.db}" 43 | ${ASH_LOG_BIN} -a "WARN: HISTFILE undefined. Exporting HISTFILE as: '${HISTFILE}'." 44 | fi 45 | if [[ ! -e "${HISTFILE}" ]] && ! touch "${HISTFILE}" &>/dev/null; then 46 | ${ASH_LOG_BIN} -a "Failed to create shell history file: '${HISTFILE}'." 47 | fi 48 | if ! chmod u+rw "${HISTFILE}" &>/dev/null; then 49 | ${ASH_LOG_BIN} -a "Failed to make shell history file readable and writeable." 50 | fi 51 | 52 | 53 | ## 54 | # Displays a message to users who manually invoked an internal-only shell 55 | # function. This is to prevent curious users from messing up internal state 56 | # accidentally. 57 | # 58 | # Internally, this function is used within protected functions like this: 59 | # function __ash_example() { 60 | # [[ "${ASH:-0}" == "0" ]] && __ash_info __ash_example && return 61 | # # Protected function code here... 62 | # } 63 | # 64 | function __ash_info() { 65 | cat << EOF_INFO 66 | ${@:-This} is an internal function of the advanced shell history utility. 67 | EOF_INFO 68 | } 69 | 70 | 71 | ## 72 | # This is invoked when a user session is exited. 73 | # 74 | # Args: 75 | # rval: The numeric exit code from the last user-entered command. 76 | # 77 | function __ash_end_session() { 78 | # Prevent users from manually invoking this function from the command line. 79 | [[ "${ASH:-0}" == "0" ]] && __ash_info __ash_end_session && return 80 | 81 | __ash_log "${@}" 82 | ${ASH_LOG_BIN} --end_session --exit ${1} 83 | } 84 | 85 | # This is executed when the user types 'exit' 86 | trap 'ASH=1 __ash_end_session ${?} ${PIPESTATUS[@]} ${pipestatus[@]}' EXIT TERM 87 | 88 | export ASH_SKIP=0 89 | 90 | 91 | ## 92 | # This is invoked immediately before each new prompt is displayed for the user. 93 | # 94 | # Args: 95 | # rval: The numeric exit code from the last user-entered command. 96 | # pipes: The set of pipe exit codes (one or more codes). 97 | # 98 | function __ash_log() { 99 | # Prevent users from manually invoking this function from the command line. 100 | [[ "${ASH:-0}" == "0" ]] && __ash_info __ash_log && return 101 | 102 | # ASH_SKIP is set only when the user presses Ctrl-C while entering a command. 103 | # Since this kills the command before it was executed, there's no history to 104 | # log before the next prompt is drawn. 105 | if [[ "${ASH_SKIP:-1}" == "1" ]]; then 106 | ASH_SKIP=0 107 | return 108 | fi 109 | 110 | local no start end cmd rval="${1}" && shift 111 | read -r no start end cmd <<< "$( __ash_last_command )" 112 | local pipes="$( sed -e 's: :_:g' <<< "${@}" )" 113 | 114 | # Log the command. 115 | ${ASH_LOG_BIN} \ 116 | -e ${rval:-0} \ 117 | -s ${start:-0} \ 118 | -f ${end:-0} \ 119 | -n ${no:-0} \ 120 | -p "${pipes:-0}" \ 121 | -c "${cmd:-UNKNOWN}" 122 | } 123 | 124 | 125 | # Protect the functions. 126 | readonly -f __ash_end_session 127 | readonly -f __ash_log 128 | 129 | #export -f __ash_end_session 130 | #export -f __ash_log 131 | -------------------------------------------------------------------------------- /shell/zsh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2018 Carl Anderson 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Prevent errors from sourcing this file mor than once. 19 | [[ -n "${ASH_SESSION_ID}" ]] && return 20 | 21 | # Make sure we are running the shell we think we are running. 22 | if ! ps ho command $$ | grep -q "zsh"; then 23 | echo "The shell process name implies you're not running zsh..." 24 | return 25 | fi 26 | 27 | # Use the default config file if one was not specified. 28 | ASH_CFG="${ASH_CFG:-/usr/local/etc/advanced-shell-history/config}" 29 | 30 | # Source the config files to set all ASH_ shell variables. 31 | set -a 32 | source "${ASH_CFG}" || exit 1 33 | set +a 34 | 35 | # Make the env config options read-only if that option is set. 36 | if [[ "${ASH_CFG_READONLY_ENV:-0}" != "0" ]]; then 37 | readonly $( sed -n -e "/^[ ]*ASH_[A-Za-z_0-9]*=.*/s:=.*::p" "${ASH_CFG}" ) 38 | fi 39 | 40 | # Source the common libraries, or complain about it in the terminal. 41 | if [[ -e "${ASH_CFG_LIB}"/common ]]; then 42 | source "${ASH_CFG_LIB}/common" || exit 1 43 | elif [[ -e "${ASH_CFG_LIB}" ]]; then 44 | echo "advanced-shell-history ERROR: Can't find common in $ASH_CFG_LIB" 45 | else 46 | echo "advanced-shell-history ERROR: Can't find ASH_CFG_LIB='$ASH_CFG_LIB'" 47 | fi 48 | 49 | 50 | # 51 | # Necessary zsh history settings that allow history collection to work: 52 | # 53 | setopt extended_history # Adds start timestamp to log file 54 | setopt inc_append_history # Forces log file to be appended after each command. 55 | (( SAVEHIST < 1 )) && export SAVEHIST=1 # SAVEHIST must be > 0 56 | 57 | 58 | ## 59 | # Invoked by __ash_log in the common library (see parent directory). 60 | # 61 | function __ash_last_command() { 62 | # Prevent users from manually invoking this function from the command line. 63 | [[ "${ASH:-0}" == "0" ]] && __ash_info __ash_last_command && return 64 | 65 | local cmd cmd_no start_ts end_ts=$( date +%s ) 66 | read start_ts <<< "$( grep "^:" "${HISTFILE}" | tail -n1 | cut -d: -f2 )" 67 | read -r cmd_no cmd <<< "$( builtin history -1 )" 68 | echo -E ${cmd_no:-0} ${start_ts:-0} ${end_ts:-0} "${cmd:-UNKNOWN}" 69 | } 70 | 71 | 72 | ## 73 | # Placeholder function. 74 | # 75 | function __ash_original_precmd() { 76 | # Prevent users from manually invoking this function from the command line. 77 | [[ "${ASH:-0}" == "0" ]] && __ash_info __ash_original_precmd && return 78 | } 79 | 80 | # This takes the definition of the original precmd (if one was defined) and 81 | # renames it to __ash_original_precmd (overwriting the placeholder above). 82 | source <( typeset -f precmd | sed -e 's/^precmd/__ash_original_precmd/' ) 83 | 84 | 85 | ## 86 | # Invoked before each new prompt is written, and after the previous command 87 | # has finished. 88 | # 89 | function precmd() { 90 | pipest_ash=( ${?} ${pipestatus[@]} ) 91 | 92 | if [[ -z ${ASH_DISABLED:-} ]]; then 93 | if [[ -z ${ASH_SESSION_ID:-} ]]; then 94 | export ASH_SESSION_ID="$( ${ASH_LOG_BIN} --get_session_id )" 95 | if [[ -n ${ASH_CFG_MOTD:-} ]]; then 96 | ${ASH_LOG_BIN} -a "${ASH_CFG_MOTD}session ${ASH_SESSION_ID}" 97 | fi 98 | else 99 | ASH=1 __ash_log ${pipest_ash[@]} 100 | fi 101 | fi 102 | ASH=1 __ash_original_precmd 103 | 104 | local rval=${pipest_ash[1]} 105 | pipest_ash=( ${pipest_ash[2,-1]} ) 106 | ${ASH_LOG_BIN} --exit ${rval:-1} 107 | } 108 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore object files. 2 | *.o 3 | 4 | # These are the binaries we're building. 5 | _ash_log 6 | ash_query 7 | 8 | # This is an OSX wart. This file is created when sed -i -e uses '-e' as the 9 | # extension for inplace backup extension. 10 | Makefile-e 11 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017 Carl Anderson 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # The VERSION variable is passed to this makefile from the main Makefile. 18 | VERSION := placeholder 19 | LOGGER := _ash_log 20 | QUERIER := ash_query 21 | EXES := ${LOGGER} ${QUERIER} 22 | OBJ_L := ${LOGGER}.o command.o config.o database.o flags.o logger.o session.o unix.o util.o 23 | OBJ_Q := ${QUERIER}.o command.o config.o database.o flags.o formatter.o logger.o session.o queries.o unix.o util.o 24 | OBJS := ${OBJ_L} ${OBJ_Q} 25 | CPPS := $(shell ls *.cpp) 26 | TRASH := ${OBJS} ${EXES} core Makefile-e 27 | CPP := g++ 28 | C := gcc 29 | FLAGS := -g -Wall -DASH_VERSION="\"${VERSION}\"" -ansi -pedantic -O2 30 | RT_LIB := -lrt 31 | 32 | .PHONY: all clean distclean new 33 | all: ${EXES} 34 | 35 | ${QUERIER}: sqlite3.o ${OBJ_Q} 36 | ${CPP} ${FLAGS} -o ${@} ${<} ${OBJ_Q} ${RT_LIB} 37 | 38 | ${LOGGER}: sqlite3.o ${OBJ_L} 39 | ${CPP} ${FLAGS} -o ${@} ${<} ${OBJ_L} ${RT_LIB} 40 | 41 | %.o: %.cpp %.hpp 42 | ${CPP} -c ${FLAGS} -o ${@} ${<} ${RT_LIB} 43 | 44 | queries.cpp: queries.hpp queries.l 45 | flex -o queries.cpp queries.l 46 | 47 | sqlite3.o: sqlite3.c 48 | ${C} -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_THREADSAFE=0 -c sqlite3.c 49 | 50 | 51 | new: clean all 52 | 53 | distclean: 54 | rm -f ${TRASH} sqlite3.o 55 | 56 | clean: 57 | rm -f ${TRASH} 58 | 59 | # This awesome target attempts to inject the exactly-right CPP dependencies 60 | # into this Makefile whenever a CPP file is edited. 61 | # 62 | # This is because, by default, the generic %.o build rule will rebuild only 63 | # when the similarly named hpp or cpp files are changed. For example, if 64 | # util.hpp has a new function added to it and we really want to rebuild 65 | # anything that #includes "util.hpp" - make has no way of knowing which files 66 | # include util.hpp unless build rules are maintained by hand. 67 | # 68 | # This handy target does some minor magic to represent the include dependencies 69 | # and keep the Makefile up-to-date. 70 | # 71 | Makefile: ${CPPS} 72 | sed -i -e '/^# DEPENDENCIES:/q' Makefile 73 | grep '^#include "' *.cpp \ 74 | | sed -e 's/:#include / /;s:"::g' \ 75 | | awk '{ \ 76 | if (cpp != "" && $$1 != cpp) { \ 77 | print cpp ":" includes; \ 78 | includes = ""; \ 79 | } \ 80 | cpp = $$1; \ 81 | includes = includes " " $$2; \ 82 | }' \ 83 | | sed -e 's/.cpp:/.o:/' >> Makefile 84 | # 85 | # Note: the dependencies below are maintained AUTOMATICALLY. 86 | # 0/ Do NOT edit or add anyting below this line - it will be DELETED! 87 | # /* for exit, getenv */ 28 | 29 | #include /* for cerr, cout, endl */ 30 | #include /* for stringstream */ 31 | 32 | 33 | DEFINE_string(alert, 'a', 0, "A message to display to the user."); 34 | DEFINE_string(command, 'c', 0, "The command to log."); 35 | DEFINE_int(command_exit, 'e', 0, "The exit code of the command to log."); 36 | DEFINE_string(command_pipe_status, 'p', 0, "The pipe states of the command to log."); 37 | DEFINE_int(command_start, 's', 0, "The timestamp when the command started."); 38 | DEFINE_int(command_finish, 'f', 0, "The timestamp when the command stopped."); 39 | DEFINE_int(command_number, 'n', 0, "The builtin shell history command number."); 40 | DEFINE_int(exit, 'x', 0, "The exit code to use when exiting."); 41 | 42 | DEFINE_flag(version, 'V', "Prints the version and exits."); 43 | DEFINE_flag(get_session_id, 'S', "Emits the session ID (or creates one)."); 44 | DEFINE_flag(end_session, 'E', "Ends the current session."); 45 | 46 | 47 | using namespace ash; 48 | using namespace flag; 49 | using namespace std; 50 | 51 | 52 | /** 53 | * Displays a brief message about how this is supposed to be used. 54 | */ 55 | void usage(ostream & out) { 56 | out << "\n\nThis program is not intended to be executed manually.\n\n" 57 | << "NOTE: See the man page for more details.\n"; 58 | Flag::show_help(out); 59 | exit(1); 60 | } 61 | 62 | 63 | int main(int argc, char ** argv) { 64 | if (getenv("ASH_DISABLED")) return FLAGS_exit; 65 | 66 | // Load the config from the environment. 67 | Config & config = Config::instance(); 68 | 69 | // Log the complete command, if debugging. 70 | stringstream ss; 71 | ss << "argv = '[0]='" << argv[0] << "'"; 72 | for (int i = 1; i < argc; ++i) 73 | ss << ",[" << i << "]='" << argv[i] << "'"; 74 | LOG(DEBUG) << ss.str(); 75 | 76 | // Show usage if executed with no args. 77 | if (argc == 1 && !config.sets("HIDE_USAGE_FOR_NO_ARGS")) { 78 | Flag::parse(&argc, &argv, true); // Sets the prog name in help output. 79 | usage(cerr); 80 | } 81 | 82 | // Parse the flags. 83 | Flag::parse(&argc, &argv, true); 84 | 85 | // Display the version and stop: -V 86 | if (FLAGS_version) { 87 | cout << ASH_VERSION << endl; 88 | return 0; 89 | } 90 | 91 | // Display a user alert: -a 'My alert' 92 | if (!FLAGS_alert.empty()) { 93 | cerr << FLAGS_alert << endl; 94 | } 95 | 96 | // Get the filename backing the history database. 97 | string db_file = config.get_string("HISTORY_DB"); 98 | if (db_file == "") { 99 | usage(cerr << "\nExpected ASH_CFG_HISTORY_DB to be defined."); 100 | } 101 | 102 | // Register the tables expected in the program. 103 | Session::register_table(); 104 | Command::register_table(); 105 | 106 | // Emit the current session number, inserting one if none exists: -S 107 | if (FLAGS_get_session_id) { 108 | Database db = Database(db_file); 109 | stringstream ss; 110 | char * id = getenv("ASH_SESSION_ID"); 111 | if (id) { 112 | ss << "select count(*) as session_cnt from sessions where id = " << id 113 | << " and duration is null;"; 114 | ResultSet * rs = db.exec(ss.str()); 115 | if (!rs || rs -> rows != 1) { 116 | cerr << "ERROR: session_id(" << id << ") not found, " 117 | << "creating new session." << endl << ss.str() << endl; 118 | id = 0; 119 | } 120 | ss.str(""); 121 | } 122 | 123 | if (id) { 124 | cout << id << endl; 125 | } else { 126 | Session session; 127 | cout << db.insert(&session) << endl; 128 | } 129 | } 130 | 131 | // Insert a command into the DB if there's a command to insert. 132 | const bool command_flag_used = !FLAGS_command.empty() 133 | || FLAGS_command_exit 134 | || !FLAGS_command_pipe_status.empty() 135 | || FLAGS_command_start 136 | || FLAGS_command_finish 137 | || FLAGS_command_number; 138 | 139 | if (command_flag_used) { 140 | Database db = Database(db_file); 141 | Command com(FLAGS_command, FLAGS_command_exit, FLAGS_command_start, 142 | FLAGS_command_finish, FLAGS_command_number, FLAGS_command_pipe_status); 143 | db.insert(&com); 144 | } 145 | 146 | // End the current session in the DB: -E 147 | if (FLAGS_end_session) { 148 | char * id = getenv("ASH_SESSION_ID"); 149 | if (id == NULL) { 150 | LOG(ERROR) << "Can't end the current session: ASH_SESSION_ID undefined."; 151 | } else { 152 | Session session; 153 | Database db = Database(db_file); 154 | db.exec(session.get_close_session_sql()); 155 | } 156 | } 157 | 158 | // Set the exit code to match what the previous command exited: -e 123 159 | return FLAGS_exit; 160 | } 161 | 162 | -------------------------------------------------------------------------------- /src/_ash_log.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __ASH_ASH_LOG__ 18 | #define __ASH_ASH_LOG__ 19 | 20 | 21 | // This SHOULD be set by the command line g++ call in the Makefile. 22 | #ifndef ASH_VERSION 23 | #define ASH_VERSION "unknown" 24 | #endif /* ASH_VERSION */ 25 | 26 | 27 | #endif /* __ASH_ASH_LOG__ */ 28 | -------------------------------------------------------------------------------- /src/ash_query.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * This program is designed to invoke saved queries in /etc/ash/queries and 19 | * ~/.ash/queries allowing multiple output formatting styles. 20 | */ 21 | 22 | #include "ash_query.hpp" 23 | 24 | #include "command.hpp" 25 | #include "config.hpp" 26 | #include "database.hpp" 27 | #include "flags.hpp" 28 | #include "formatter.hpp" 29 | #include "logger.hpp" 30 | #include "queries.hpp" 31 | #include "session.hpp" 32 | 33 | #include 34 | #include 35 | #include 36 | 37 | using namespace ash; 38 | using namespace flag; 39 | using namespace std; 40 | 41 | 42 | DEFINE_string(database, 'd', 0, "A history database to query."); 43 | DEFINE_string(format, 'f', 0, "A format to display results."); 44 | DEFINE_int(limit, 'l', 0, "Limit the number of rows returned."); 45 | DEFINE_string(print_query, 'p', 0, "Print the query SQL."); 46 | DEFINE_string(query, 'q', 0, "The name of the saved query to execute."); 47 | 48 | DEFINE_flag(list_formats, 'F', "Display all available formats."); 49 | DEFINE_flag(hide_headings, 'H', "Hide column headings from query results."); 50 | DEFINE_flag(list_queries, 'Q', "Display all saved queries."); 51 | DEFINE_flag(version, 0, "Show the version and exit."); 52 | 53 | 54 | typedef map RowsType; 55 | 56 | 57 | /** 58 | * Prints a mapping of string to string using a fixed width between columns. 59 | */ 60 | void display(ostream & out, const RowsType & rows, const string & name) { 61 | RowsType::const_iterator i, e; 62 | const size_t XX = 4; // The number of spaces between columns. 63 | size_t widths[2] = {name.size() + XX, string("Description").size() + XX}; 64 | 65 | // Calculate the required widths for each column. 66 | for (i = rows.begin(), e = rows.end(); i != e; ++i) { 67 | widths[0] = max(widths[0], XX + (i -> first).size()); 68 | widths[1] = max(widths[1], XX + (i -> second).size()); 69 | } 70 | 71 | // Output the headings and the rows. 72 | cout << left 73 | << setw(widths[0]) << name 74 | << setw(widths[1]) << "Description" << endl; 75 | for (i = rows.begin(), e = rows.end(); i != e; ++i) { 76 | cout << left 77 | << setw(widths[0]) << i -> first 78 | << setw(widths[1]) << i -> second << endl; 79 | } 80 | } 81 | 82 | 83 | /** 84 | * Executes a query, printing the results to stdout according to the 85 | * user-chosen output format. 86 | */ 87 | int execute(const string & sql) { 88 | Config & config = Config::instance(); 89 | 90 | // Get the filename backing the database we are about to query. 91 | string db_file(FLAGS_database); 92 | if (db_file == "") { 93 | if (config.get_string("HISTORY_DB") == "") { 94 | cerr << "Expected either --database or ASH_CFG_HISTORY_DB " 95 | << "to be defined." << endl; 96 | return 1; 97 | } 98 | db_file = config.get_string("HISTORY_DB"); 99 | } 100 | 101 | // Prepare the DB for reading. 102 | Session::register_table(); 103 | Command::register_table(); 104 | Database db(db_file); 105 | 106 | // Get the intended Formatter before executing the query. 107 | string format = FLAGS_format == "" 108 | ? config.get_string("DEFAULT_FORMAT", "aligned") 109 | : FLAGS_format; 110 | Formatter * formatter = Formatter::lookup(format); 111 | if (!formatter) { 112 | cerr << "\nUnknown format: '" << format << "'" << endl; 113 | display(cerr << '\n', Formatter::get_desc(), "Format"); 114 | return 1; 115 | } 116 | 117 | // Execute the query and display any results. 118 | ResultSet * rs = db.exec(sql, FLAGS_limit); 119 | formatter -> show_headings(!FLAGS_hide_headings); 120 | formatter -> insert(rs, cout); 121 | if (rs) delete rs; 122 | return 0; 123 | } 124 | 125 | 126 | /** 127 | * Query the history database. 128 | */ 129 | int main(int argc, char ** argv) { 130 | // Load the config from the environment. 131 | Config & config = Config::instance(); 132 | 133 | if (argc == 1) { // No flags. 134 | if (config.sets("DEFAULT_QUERY")) { 135 | return execute(config.get_string("DEFAULT_QUERY")); 136 | } 137 | if (!config.sets("HIDE_USAGE_FOR_NO_ARGS")) { 138 | Flag::parse(&argc, &argv, true); // Sets the prog name in help output. 139 | Flag::show_help(cerr); 140 | } 141 | return 1; 142 | } 143 | 144 | // Parse the flags, removing from argv and argc. 145 | Flag::parse(&argc, &argv, true); 146 | 147 | // Abort if unrecognized flags were used on the command line. 148 | if (argc != 0 && !config.sets("IGNORE_UNKNOWN_FLAGS")) { 149 | cerr << "unrecognized flag: " << argv[0] << endl; 150 | Flag::show_help(cerr); 151 | return 1; 152 | } 153 | 154 | // Display version, if that's all that was wanted. 155 | if (FLAGS_version) { 156 | cout << ASH_VERSION << endl; 157 | return 0; 158 | } 159 | 160 | // Display available query names, if requested. 161 | if (FLAGS_list_queries) { 162 | display(cout, Queries::get_desc(), "Query"); 163 | return 0; 164 | } 165 | 166 | // Initialize the available formatters. 167 | CsvFormatter::init(); 168 | NullFormatter::init(); 169 | SpacedFormatter::init(); 170 | GroupedFormatter::init(); 171 | 172 | // Diaplay the available format names. 173 | if (FLAGS_list_formats) { 174 | display(cout, Formatter::get_desc(), "Format"); 175 | return 0; 176 | } 177 | 178 | // Print the requested query (both generic and actual, if different). 179 | if (FLAGS_print_query != "") { 180 | string sql = Queries::get_sql(FLAGS_print_query); 181 | string raw = Queries::get_raw_sql(FLAGS_print_query); 182 | if (raw == "") { 183 | cout << "Query not found: " << FLAGS_print_query << "\nAvailable:\n"; 184 | display(cout, Queries::get_desc(), "Query"); 185 | return 1; 186 | } 187 | cout << "Query: " << FLAGS_print_query << endl; 188 | if (raw != sql) { 189 | cout << "Template Form:\n" << raw << "\nActual SQL:\n"; 190 | } 191 | cout << sql << endl; 192 | return 0; 193 | } 194 | 195 | // Make sure the requested query exists. 196 | string sql = Queries::get_sql(FLAGS_query); 197 | if (sql == "") { 198 | cout << "Query not found: " << FLAGS_query << "\nAvailable:\n"; 199 | display(cout, Queries::get_desc(), "Query"); 200 | return 1; 201 | } 202 | 203 | // Execute the requested query. 204 | return execute(sql); 205 | } 206 | -------------------------------------------------------------------------------- /src/ash_query.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __ASH_QUERY__ 18 | #define __ASH_QUERY__ 19 | 20 | 21 | #ifndef ASH_VERSION 22 | #define ASH_VERSION "unknown" 23 | #endif /* ASH_VERSION */ 24 | 25 | 26 | #endif /* __ASH_QUERY__ */ 27 | -------------------------------------------------------------------------------- /src/command.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "command.hpp" 18 | 19 | #include "unix.hpp" 20 | #include "util.hpp" 21 | 22 | #include 23 | 24 | 25 | using namespace ash; 26 | using std::stringstream; 27 | 28 | 29 | /** 30 | * Registers this table for use in the Database. 31 | */ 32 | void Command::register_table() { 33 | string name = "commands"; 34 | stringstream ss; 35 | ss << "CREATE TABLE IF NOT EXISTS " << name << " (\n" 36 | << " id integer primary key autoincrement,\n" 37 | << " session_id integer not null,\n" 38 | << " shell_level integer not null,\n" 39 | << " command_no integer,\n" 40 | << " tty varchar(20) not null,\n" 41 | << " euid int(16) not null,\n" 42 | << " cwd varchar(256) not null,\n" 43 | << " rval int(5) not null,\n" 44 | << " start_time integer not null,\n" 45 | << " end_time integer not null,\n" 46 | << " duration integer not null,\n" 47 | << " pipe_cnt int(3),\n" 48 | << " pipe_vals varchar(80),\n" 49 | << " command varchar(1000) not null,\n" 50 | << "UNIQUE(session_id, command_no)\n" 51 | << ");"; 52 | DBObject::register_table(name, ss.str()); 53 | } 54 | 55 | 56 | /** 57 | * Initializes a Command object by gathering various system data. 58 | */ 59 | Command::Command(const string command, const int rval, const int start_ts, 60 | const int end_ts, const int number, const string pipes) 61 | { 62 | values["session_id"] = unix::env_int("ASH_SESSION_ID"); 63 | values["shell_level"] = unix::env_int("SHLVL"); 64 | values["command_no"] = Util::to_string(number); 65 | values["tty"] = unix::tty(); 66 | values["euid"] = unix::euid(); 67 | if (rval == 0 && command.find("cd") == 0) { 68 | values["cwd"] = unix::env("OLDPWD"); 69 | } else { 70 | values["cwd"] = unix::cwd(); 71 | } 72 | values["rval"] = Util::to_string(rval); 73 | values["start_time"] = Util::to_string(start_ts); 74 | values["end_time"] = Util::to_string(end_ts); 75 | values["duration"] = Util::to_string(end_ts - start_ts); 76 | int pipe_cnt = 1; 77 | for (string::const_iterator i = pipes.begin(), e = pipes.end(); i != e; ++i) 78 | if ((*i) == '_') ++pipe_cnt; 79 | values["pipe_cnt"] = Util::to_string(pipe_cnt); 80 | values["pipe_vals"] = quote(pipes); 81 | values["command"] = quote(command); 82 | } 83 | 84 | 85 | /** 86 | * Required since the base class declares a virtual dtor. 87 | */ 88 | Command::~Command() { 89 | // Nothing to do. 90 | } 91 | 92 | 93 | /** 94 | * Returns the name of the backing table. 95 | */ 96 | const string Command::get_name() const { 97 | return "commands"; 98 | } 99 | 100 | -------------------------------------------------------------------------------- /src/command.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __ASH_COMMAND__ 18 | #define __ASH_COMMAND__ 19 | 20 | #include 21 | 22 | #include "database.hpp" 23 | 24 | using std::string; 25 | 26 | namespace ash { 27 | 28 | 29 | /** 30 | * This class represents a user-entered command to be saved in the database. 31 | */ 32 | class Command : public DBObject { 33 | public: 34 | static void register_table(); 35 | 36 | public: 37 | Command(const string command, const int rval, const int start, 38 | const int end, const int num, const string pipes); 39 | virtual ~Command(); 40 | 41 | virtual const string get_name() const; 42 | }; 43 | 44 | 45 | } // namespace ash 46 | 47 | #endif /* __ASH_COMMAND__ */ 48 | -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | #include "config.hpp" 19 | 20 | #include /* for getenv */ 21 | #include /* for environ */ 22 | 23 | #include 24 | 25 | using namespace ash; 26 | using namespace std; 27 | 28 | 29 | extern char ** environ; /* populated by unistd.h */ 30 | 31 | 32 | /** 33 | * Construct a Config object, setting defaults. 34 | */ 35 | Config::Config() 36 | : values(), is_loaded(false) 37 | { 38 | // NOTHING TO DO! 39 | } 40 | 41 | 42 | /** 43 | * Returns a char * value for an environment variable prefixed with ASH_CFG_. 44 | */ 45 | char * get_ash_env(const string & key) { 46 | if (key.find("ASH_CFG_", 0, 8) == 0) 47 | return getenv(key.c_str()); 48 | return getenv((string("ASH_CFG_") + key).c_str()); 49 | } 50 | 51 | 52 | /** 53 | * Returns true if the environment actually contains this variable. 54 | */ 55 | bool Config::has(const string & key) const { 56 | return get_ash_env(key) != NULL; 57 | } 58 | 59 | 60 | /** 61 | * Returns true if the environment contains this value and it equals 'true'. 62 | */ 63 | bool Config::sets(const string & key, const bool dv) const { 64 | char * env = get_ash_env(key); 65 | return env ? "true" == string(env) : dv; 66 | } 67 | 68 | 69 | /** 70 | * Returns the int value of the requested environment variable. 71 | */ 72 | int Config::get_int(const string & key, const int dv) const { 73 | char * env = get_ash_env(key); 74 | return env ? atoi(env) : dv; 75 | } 76 | 77 | 78 | /** 79 | * Returns the char * value of the requested environment variable. 80 | */ 81 | const char * Config::get_cstring(const string & key, const char * dv) const { 82 | char * env = get_ash_env(key); 83 | return env ? string(env).c_str() : dv; 84 | } 85 | 86 | 87 | /** 88 | * Returns the string value of the requested environment variable. 89 | */ 90 | string Config::get_string(const string & key, const string & dv) const { 91 | char * env = get_ash_env(key); 92 | return env ? string(env) : dv; 93 | } 94 | 95 | 96 | /** 97 | * Returns a singleton Config instance. 98 | */ 99 | Config & Config::instance() { 100 | static Config _instance; 101 | if (!_instance.is_loaded) { 102 | // Find all environment variables matching a common prefix ASH_CFG_ 103 | for (int i = 0; environ[i] != NULL; ++i) { 104 | string line = environ[i]; 105 | if (line.substr(0, 8) == "ASH_CFG_") { 106 | int first_equals = line.find_first_of('='); 107 | string key = line.substr(8, first_equals - 8); 108 | string value = line.substr(first_equals + 1); 109 | _instance.values[key] = value; 110 | } 111 | } 112 | _instance.is_loaded = true; 113 | } 114 | return _instance; 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/config.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __ASH_CONFIG__ 18 | #define __ASH_CONFIG__ 19 | 20 | #include 21 | #include 22 | 23 | using std::map; 24 | using std::string; 25 | 26 | namespace ash { 27 | 28 | 29 | /** 30 | * This class contains all the environment variable values for variables named 31 | * with a common prefix: ASH_CFG_ 32 | */ 33 | class Config { 34 | // STATIC: 35 | public: 36 | static Config & instance(); 37 | 38 | // NON-STATIC: 39 | public: 40 | bool has(const string & key) const; 41 | bool sets(const string & key, const bool dv=false) const; 42 | int get_int(const string & key, const int dv=1) const; 43 | const char * get_cstring(const string & key, const char * dv="") const; 44 | string get_string(const string & key, const string & dv="") const; 45 | 46 | private: 47 | Config(); 48 | 49 | private: 50 | map values; 51 | bool is_loaded; 52 | 53 | private: // DISALLOWED: 54 | Config(const Config & other); 55 | Config & operator=(const Config & other); 56 | }; 57 | 58 | 59 | } // namespace ash 60 | 61 | #endif /* __ASH_CONFIG__ */ 62 | -------------------------------------------------------------------------------- /src/database.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "database.hpp" 18 | 19 | #include "config.hpp" 20 | #include "logger.hpp" 21 | 22 | #include /* for errno */ 23 | #include /* for stat */ 24 | #include /* for timeval */ 25 | #include /* for fopen */ 26 | #include /* for rand, srand */ 27 | #include /* for strerror */ 28 | #include /* for time */ 29 | #include /* for getpid */ 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | // This hack silences a warning when compiling on a 64 bit platform with 38 | // -ansi and -pedantic flags enabled. 39 | // The original g++ complaint is that 'long long' is deprecated. 40 | #ifdef __LP64__ 41 | #define SQLITE_INT64_TYPE long int 42 | #endif 43 | #include "sqlite3.h" 44 | 45 | using namespace ash; 46 | using namespace std; 47 | 48 | 49 | /** 50 | * A list of the registered tables names for the DB. 51 | */ 52 | vector DBObject::table_names; 53 | 54 | 55 | /** 56 | * A list of the queries that create registered tables for the DB. 57 | */ 58 | list DBObject::create_tables; 59 | 60 | 61 | /** 62 | * Initialize a ResultSet. 63 | */ 64 | ResultSet::ResultSet(const HeadersType & h, const DataType & d) 65 | : headers(h), 66 | data(d), 67 | rows(d.size()), 68 | columns(h.size()) 69 | { 70 | // Nothing to do! 71 | } 72 | 73 | 74 | /** 75 | * Create a new Database, creating a new backing file if necessary. 76 | */ 77 | Database::Database(const string & filename) 78 | : db_filename(filename), db(0) 79 | { 80 | struct stat file; 81 | // Test that the history file exists, if not, create it. 82 | if (stat(db_filename.c_str(), &file)) { 83 | FILE * created_file = fopen(db_filename.c_str(), "w+e"); 84 | if (!created_file) { 85 | LOG(FATAL) << "failed to create new DB file: " << db_filename << endl; 86 | } 87 | fclose(created_file); 88 | } 89 | 90 | // Open the DB, if failure, abort. 91 | if (sqlite3_open(db_filename.c_str(), &db)) { 92 | LOG(FATAL) << "Failed to open " << db_filename << "\nError: " 93 | << sqlite3_errmsg(db) << endl; 94 | } 95 | 96 | // Init the DB if it is missing the main tables. 97 | size_t registered = DBObject::table_names.size(); 98 | 99 | stringstream ss; 100 | ss << "select count(*) as table_count " 101 | << "from sqlite_master " 102 | << "where type = 'table' and tbl_name in ("; 103 | 104 | // List the table names registered by the code. 105 | if (registered > 0) ss << DBObject::quote(DBObject::table_names[0]); 106 | for (size_t i = 1; i < registered; ++i) 107 | ss << ", " << DBObject::quote(DBObject::table_names[i]); 108 | 109 | ss << ");"; 110 | string query = ss.str(); 111 | 112 | ResultSet * rs = exec(query.c_str()); 113 | size_t defined_tables = 114 | rs && rs -> rows == 1 ? atoi(rs -> data[0][0].c_str()) : 0; 115 | if (rs) delete rs; 116 | if (defined_tables == registered) return; // Normal case. 117 | 118 | // Initialize the DB if it's not already set up. 119 | init_db(); 120 | 121 | // Log a warning if there was an unexpected number of tables. 122 | if (defined_tables > registered) { 123 | LOG(WARNING) << "Expected " << registered 124 | << " tables to be defined, found " << defined_tables << " instead."; 125 | } 126 | } 127 | 128 | 129 | /** 130 | * Close the Database and free internal resources. 131 | */ 132 | Database::~Database() { 133 | if (db) { 134 | sqlite3_close(db); 135 | db = 0; 136 | } 137 | } 138 | 139 | 140 | /** 141 | * A No-Op callback that returns 0. 142 | */ 143 | int NOOPCallback(void * ignored, int rows, char ** cols, char ** col_names) { 144 | // Nothing to do in this callback. 145 | return 0; 146 | } 147 | 148 | 149 | /** 150 | * Executes the create-tables query to initialize this database. 151 | */ 152 | void Database::init_db() { 153 | const string & create_tables = DBObject::get_create_tables(); 154 | char * error = 0; 155 | if (sqlite3_exec(db, create_tables.c_str(), NOOPCallback, 0, &error)) { 156 | cerr << "Failed to create tables:\n" 157 | << create_tables 158 | << "Error:\n" 159 | << error << endl; 160 | sqlite3_free(error); 161 | error = 0; 162 | } 163 | } 164 | 165 | 166 | /** 167 | * This method is only to be invoked if the database were locked when an insert 168 | * or other query was attempted. 169 | * 170 | * This method does a lot to make sure a good sleep is had. It checks the 171 | * configured sleep settings: ASH_CFG_DB_FAIL_RANDOM_TIMEOUT and 172 | * ASH_CFG_DB_FAIL_TIMEOUT. It also tries to make sure that the specified sleep 173 | * amount is honored. 174 | */ 175 | void ash_sleep() { 176 | Config & config = Config::instance(); 177 | 178 | int retries = config.get_int("DB_MAX_RETRIES", -1); 179 | if (retries < 0) retries = 5; 180 | 181 | int fail_ms = config.get_int("DB_FAIL_TIMEOUT", -1); 182 | if (fail_ms < 0) fail_ms = 0; 183 | 184 | int random_ms = config.get_int("DB_FAIL_RANDOM_TIMEOUT", -1); 185 | if (random_ms < 0) random_ms = 0; 186 | 187 | // Sleep a number amount of ms within the parameters. 188 | unsigned long int ms = fail_ms; 189 | if (random_ms > 0) { 190 | // Randomize with both the time and the PID. Since the database being 191 | // locked is likely a collission with another _ash_log - time along will 192 | // randomize both processes to generate the same random numbers. 193 | srand(time(0) ^ getpid()); 194 | ms += rand() % random_ms; 195 | } 196 | LOG(INFO) << "Sleeping " << ms << " milliseconds."; 197 | if (ms == 0) return; 198 | 199 | // Measure a timestamp to count how long we actually slept. 200 | struct timespec before_ts, after_ts; 201 | if (clock_gettime(CLOCK_MONOTONIC, &before_ts)) { 202 | perror("clock_gettime failed"); 203 | } 204 | 205 | unsigned int sleep_attempts = retries * 2; 206 | struct timespec to_sleep, remaining; 207 | to_sleep.tv_sec = ms / 1000L; 208 | to_sleep.tv_nsec = (ms % 1000L) * 1000000L; 209 | 210 | while (to_sleep.tv_sec || to_sleep.tv_nsec) { 211 | // Sanity check to make sure we aren't looping infinitely. 212 | if (sleep_attempts == 0) { 213 | LOG(WARNING) << "Sleep break triggered. CLOCK_MONOTONIC may not work as " 214 | << "expected on your system. Or the database may be " 215 | << "extremely busy with concurrent requests."; 216 | break; // safety 217 | } 218 | --sleep_attempts; 219 | 220 | // Sleep and verify the return code. 221 | int rval = clock_nanosleep(CLOCK_MONOTONIC, 0, &to_sleep, &remaining); 222 | switch (rval) { 223 | case -1: 224 | LOG(ERROR) << "Failed to clock_nanosleep: " << strerror(errno); 225 | continue; 226 | case 0: break; 227 | case EFAULT: 228 | LOG(ERROR) << "clock_nanosleep: EFAULT sleeping " << ms << " ms."; 229 | break; 230 | case EINTR: 231 | LOG(ERROR) << "clock_nanosleep: EINTR sleeping " << ms << " ms."; 232 | break; 233 | case EINVAL: 234 | LOG(ERROR) << "clock_nanosleep: EINVAL sleeping (tv_sec=" 235 | << to_sleep.tv_sec << ", tv_nsec=" 236 | << to_sleep.tv_nsec << ")"; 237 | break; 238 | default: 239 | LOG(ERROR) << "Unexpected rval from clock_nanosleep: " << rval; 240 | } 241 | 242 | // If there is any time remaining, prepare to sleep again. 243 | if (remaining.tv_sec == 0 244 | && remaining.tv_nsec > 0 245 | && remaining.tv_nsec <= 999999999 246 | && remaining.tv_nsec < to_sleep.tv_nsec) 247 | { 248 | LOG(DEBUG) << "slept (to_sleep.tv_nsec - remaining.tv_nsec = " 249 | << to_sleep.tv_nsec << " - " << remaining.tv_nsec << " = " 250 | << (to_sleep.tv_nsec - remaining.tv_nsec) << ")"; 251 | to_sleep.tv_sec = remaining.tv_sec; 252 | to_sleep.tv_nsec = remaining.tv_nsec; 253 | } 254 | } 255 | 256 | // Check to see how long has passed since ash_sleep began. 257 | if (clock_gettime(CLOCK_MONOTONIC, &after_ts)) { 258 | perror("clock_gettime failed"); 259 | } 260 | 261 | unsigned long int slept = 262 | (after_ts.tv_sec - before_ts.tv_sec) * 1000 + 263 | (after_ts.tv_nsec - before_ts.tv_nsec) / 1000000L; 264 | 265 | LOG(INFO) << "Slept " << slept << " milliseconds."; 266 | 267 | if (ms > slept) { 268 | // This should never happen because of the while loop above. 269 | LOG(ERROR) << "Failed to sleep " << ms << " ms between failures."; 270 | } 271 | 272 | if (slept > (ms + 1) * 100) { 273 | LOG(ERROR) << "Major clock problem detected. Requested sleep of: " << ms 274 | << " ms took " << slept << " ms to complete."; 275 | } 276 | } 277 | 278 | 279 | /** 280 | * Inserts the DBObject, returning the new ROWID. 281 | */ 282 | long int Database::insert(DBObject * object) const { 283 | if (!object) return 0; 284 | exec(object -> get_sql()); 285 | return sqlite3_last_insert_rowid(db); 286 | } 287 | 288 | 289 | /** 290 | * Returns a prepared statement if possible. This method will attempt to 291 | * retry preparing the statement up to ASH_CFG_DB_MAX_RETRIES times before 292 | * admitting defeat and exiting the program with a FATAL error. 293 | */ 294 | sqlite3_stmt * Database::prepare_stmt(const string & query) const { 295 | sqlite3_stmt * ps = 0; 296 | 297 | Config & config = Config::instance(); 298 | int max_retries = config.get_int("DB_MAX_RETRIES", -1); 299 | if (max_retries <= 0) max_retries = 5; 300 | int tries = max_retries + 1; 301 | 302 | try_prepare: 303 | sqlite3_prepare_v2(db, query.c_str(), query.length(), &ps, 0); 304 | if (ps) return ps; 305 | int error_code = sqlite3_errcode(db); 306 | switch (error_code) { 307 | case SQLITE_LOCKED: // fallthrough 308 | case SQLITE_BUSY: 309 | LOG(DEBUG) << "Database is busy while preparing a statement."; 310 | sqlite3_finalize(ps); 311 | if (--tries > 0) { 312 | LOG(DEBUG) << "Sleeping and trying to prepare statement again."; 313 | ash_sleep(); 314 | goto try_prepare; 315 | } 316 | LOG(FATAL) << "Failed to prepare statement after " << max_retries 317 | << " failed attempts."; 318 | return 0; // unreachable 319 | default: 320 | LOG(FATAL) << "Unexpected error code while preparing statement: " 321 | << error_code; 322 | return 0; // unreachable 323 | } 324 | } 325 | 326 | 327 | /** 328 | * Execute a query or abort the program with the DB error message. 329 | */ 330 | ResultSet * Database::exec(const string & query, const int limit) const { 331 | // Load the relevant configured values. 332 | Config & config = Config::instance(); 333 | 334 | int max_retries = config.get_int("DB_MAX_RETRIES", -1); 335 | if (max_retries <= 0) max_retries = 5; 336 | int tries = max_retries + 1, fetched = 0; 337 | 338 | ResultSet::HeadersType headers; 339 | ResultSet::DataType results; 340 | stringstream ss; 341 | sqlite3_stmt * ps = prepare_stmt(query); 342 | unsigned int rows, columns = sqlite3_column_count(ps); 343 | 344 | // YES, this is a GOTO target. This is used to implement the retry logic. 345 | // If a query fails because of a lock, it may goto this block to retry. 346 | try_sql: 347 | fetched = 0; 348 | for (rows = 0; fetched < limit || limit <= 0; ++rows) { 349 | int result = sqlite3_step(ps); 350 | switch (result) { 351 | // TODO(cpa): add more cases to handle errors. 352 | case SQLITE_ROW: 353 | // build the list of header names, if this is the first row fetched. 354 | if (headers.empty()) { 355 | for (size_t c = 0; c < columns; ++c) { 356 | ss.str(""); 357 | ss << sqlite3_column_name(ps, c); 358 | headers.push_back(ss.str()); 359 | } 360 | } 361 | // Add the row data. 362 | results.push_back(ResultSet::RowType()); 363 | for (size_t c = 0; c < columns; ++c) { 364 | ss.str(""); 365 | if (sqlite3_column_text(ps, c)) { 366 | ss << sqlite3_column_text(ps, c); 367 | } 368 | results.back().push_back(ss.str()); 369 | } 370 | ++fetched; 371 | continue; // for loop 372 | case SQLITE_CONSTRAINT: 373 | // Note: there is no point retrying this type of error. 374 | LOG(DEBUG) << "constraint violation executing: '" << query << "'"; 375 | goto finalize; 376 | case SQLITE_DONE: 377 | goto finalize; 378 | case SQLITE_LOCKED: // Fallthrough. 379 | case SQLITE_BUSY: { 380 | // Abort if we are out of attempts. 381 | if (tries <= 0) { 382 | LOG(FATAL) << "Failed to unlock db: " << sqlite3_errmsg(db) 383 | << "\nGave up after " << max_retries << " failures." 384 | << "\nExecuting: '" << query << "'\n"; 385 | } 386 | 387 | // Sleep some random number of milliseconds. 388 | ash_sleep(); 389 | 390 | // Reset the prepared statement. 391 | sqlite3_finalize(ps); 392 | ps = prepare_stmt(query); 393 | 394 | // Decrement the try count and jump. 395 | --tries; 396 | LOG(WARNING) << "Database was locked, tries remaining: " << tries; 397 | goto try_sql; 398 | } 399 | default: 400 | sqlite3_finalize(ps); 401 | // TODO(cpa): remove this cerr line once the FATAL errors are redirected to stderr by default. 402 | cerr << "unknown sqlite3_step code: " << result << " executing '" 403 | << query << "'\nError:\n" << sqlite3_errmsg(db) << endl; 404 | LOG(FATAL) << "unknown sqlite3_step code: " << result 405 | << " executing '" << query << "'\nError:\n" 406 | << sqlite3_errmsg(db); 407 | } // switch 408 | } // for loop 409 | 410 | // Yes, another GOTO target. This is used because there is a switch statement 411 | // within a for loop and it's convenient to break the loop within the switch. 412 | finalize: 413 | sqlite3_finalize(ps); 414 | 415 | return rows ? new ResultSet(headers, results) : 0; 416 | } 417 | 418 | 419 | /** 420 | * DB_OBJECT CODE BELOW: 421 | */ 422 | 423 | 424 | /** 425 | * Returns a query that creates the table schema for the DB. 426 | */ 427 | const string DBObject::get_create_tables() { 428 | stringstream ss; 429 | ss << "PRAGMA foreign_keys=OFF;" 430 | << "BEGIN TRANSACTION;"; 431 | typedef list::iterator it; 432 | for (it i = create_tables.begin(), e = create_tables.end(); i != e; ++i) { 433 | ss << *i << "; "; 434 | } 435 | ss << "COMMIT;"; 436 | return ss.str(); 437 | } 438 | 439 | 440 | /** 441 | * Adds a create-table query to the list of create-table queries. 442 | */ 443 | void DBObject::register_table(const string & name, const string & sql) { 444 | table_names.push_back(name); 445 | create_tables.push_back(sql); 446 | } 447 | 448 | 449 | /** 450 | * Returns a quoted string value using a char * input. 451 | */ 452 | const string DBObject::quote(const char * value) { 453 | return value ? quote(string(value)) : "null"; 454 | } 455 | 456 | 457 | /** 458 | * Returns a quoted string suitable for insertion into the DB. 459 | * Converts an empty string to null. Removes unprintable characters. 460 | * Replaces all single-quotes with double single quotes in the output string. 461 | */ 462 | const string DBObject::quote(const string & in) { 463 | if (in.empty()) return "null"; 464 | string out = "'"; 465 | char c; 466 | for (string::const_iterator i = in.begin(), e = in.end(); i != e; ++i) { 467 | c = *i; 468 | switch (c) { 469 | case '\n': // fallthrough 470 | case '\t': 471 | out.push_back(c); 472 | break; 473 | case '\'': 474 | out.push_back(c); // fallthrough 475 | default: 476 | if (isprint(c)) out.push_back(c); 477 | } 478 | } 479 | out.push_back('\''); 480 | return out; 481 | } 482 | 483 | 484 | /** 485 | * Construct an empty DB Object. 486 | */ 487 | DBObject::DBObject() { 488 | // Nothing to do here. 489 | } 490 | 491 | 492 | /** 493 | * REQUIRED since it was declared virtual. 494 | */ 495 | DBObject::~DBObject() { 496 | // Nothing to do here. 497 | } 498 | 499 | 500 | /** 501 | * Returns the SQL statement needed to insert a concrete instance of this 502 | * class. 503 | */ 504 | const string DBObject::get_sql() const { 505 | typedef map::const_iterator c_iter; 506 | 507 | stringstream ss; 508 | ss << "INSERT INTO " << get_name() << " ("; 509 | 510 | // Insert the column names. 511 | for (c_iter i = values.begin(), e = values.end(); i != e; ) { 512 | ss << (i -> first); 513 | if (++i == e) break; 514 | ss << ", "; 515 | } 516 | 517 | ss << ") VALUES ("; 518 | // Insert the values. 519 | for (c_iter i = values.begin(), e = values.end(); i != e; ) { 520 | ss << (i -> second); 521 | if (++i == e) break; 522 | ss << ", "; 523 | } 524 | 525 | ss << "); "; 526 | 527 | return ss.str(); 528 | } 529 | 530 | -------------------------------------------------------------------------------- /src/database.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __ASH_DATABASE__ 18 | #define __ASH_DATABASE__ 19 | 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | using std::list; 27 | using std::map; 28 | using std::string; 29 | using std::vector; 30 | 31 | struct sqlite3; // Forward declaration. 32 | struct sqlite3_stmt; // Forward declaration. 33 | 34 | namespace ash { 35 | 36 | class Database; // Forward declaration. 37 | class DBObject; // Forward declaration. 38 | 39 | 40 | /** 41 | * This is the result of a query that selects multiple rows. 42 | */ 43 | class ResultSet { 44 | public: 45 | typedef list HeadersType; 46 | typedef vector RowType; 47 | typedef vector DataType; 48 | 49 | public: 50 | ~ResultSet() {} 51 | 52 | private: 53 | ResultSet(const HeadersType & headers, const DataType & data); 54 | 55 | public: 56 | const HeadersType headers; 57 | const DataType data; 58 | const size_t rows, columns; 59 | 60 | // DISALLOWED: 61 | private: 62 | ResultSet(const ResultSet & other); // disallowed. 63 | ResultSet & operator = (const ResultSet & other); // disallowed. 64 | 65 | friend class Database; 66 | }; 67 | 68 | 69 | /** 70 | * This class abstracts a backing sqlite3 database. 71 | */ 72 | class Database { 73 | public: 74 | Database(const string & filename); 75 | virtual ~Database(); 76 | 77 | ResultSet * exec(const string & query, const int limit=0) const; 78 | 79 | long int insert(DBObject * object) const; 80 | 81 | void init_db(); 82 | 83 | private: 84 | sqlite3_stmt * prepare_stmt(const string & query) const; 85 | 86 | private: 87 | const string db_filename; 88 | sqlite3 * db; 89 | }; 90 | 91 | 92 | /* abstract */ 93 | class DBObject { 94 | // STATIC: 95 | public: 96 | static const string quote(const char * value); 97 | static const string quote(const string & value); 98 | static const string get_create_tables(); 99 | 100 | protected: 101 | static void register_table(const string & name, const string & sql); 102 | 103 | protected: 104 | static list create_tables; 105 | static vector table_names; 106 | 107 | // NON-STATIC: 108 | protected: 109 | DBObject(); 110 | virtual ~DBObject(); 111 | 112 | virtual const string get_name() const = 0; // abstract 113 | virtual const string get_sql() const; 114 | 115 | map values; 116 | 117 | // DISALLOWED: 118 | private: 119 | DBObject(const DBObject & other); // disabled 120 | DBObject & operator =(const DBObject & other); // disabled 121 | 122 | friend class Database; 123 | }; 124 | 125 | 126 | } // namespace ash 127 | 128 | #endif /* __ASH_DATABASE__ */ 129 | 130 | -------------------------------------------------------------------------------- /src/flags.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "flags.hpp" 18 | 19 | #include /* for isgraph */ 20 | #include /* for basename */ 21 | #include /* for atoi */ 22 | #include /* for strdup */ 23 | 24 | namespace flag { 25 | 26 | using namespace std; 27 | 28 | 29 | static unsigned int longest_long_name = 0; 30 | static string prog_name; 31 | 32 | 33 | // Special Flags. 34 | DEFINE_flag(help, 0, "Display flags for this command."); 35 | 36 | 37 | /** 38 | * Inserts the default help output for all registered flags into the argument 39 | * ostream. 40 | * 41 | * sh$ ash_log --help 42 | * Usage: ash_log [options] 43 | * --help Display this message. 44 | * sh$ 45 | */ 46 | void Flag::show_help(ostream & out) { 47 | char * program_name = strdup(prog_name.c_str()); 48 | out << "\nUsage: " << basename(program_name); 49 | 50 | list & flags = Flag::instances(); 51 | if (flags.empty()) return; 52 | 53 | out << " [options]"; 54 | for (list::iterator i = flags.begin(); i != flags.end(); ++i) { 55 | out << "\n" << **i; 56 | } 57 | out << "\n" << endl; 58 | } 59 | 60 | 61 | /** 62 | * Parses the main method argc and argv values. If the remove_flags argument is 63 | * true - the flags and values are removed from the flag list. 64 | */ 65 | int Flag::parse(int * p_argc, char *** p_argv, const bool remove_flags) { 66 | int argc = *p_argc; 67 | char ** argv = *p_argv; 68 | 69 | // Grab the program name from argv[0] for --help output later. 70 | prog_name = argv[0]; 71 | 72 | // Put the options into an array, as getopt expects them. 73 | struct option * options = new struct option[Flag::options().size() + 1]; 74 | int x = 0; 75 | typedef list::iterator iter; 76 | for (iter i = Flag::options().begin(), e = Flag::options().end(); i != e; ++i) { 77 | options[x++] = *i; 78 | } 79 | // This sentinel is needed to prevent the getopt library from segfaulting 80 | // when an unknown long option is seen first on the list of flags. 81 | struct option sentinel = {0, 0, 0, 0}; 82 | options[x] = sentinel; 83 | 84 | // Parse the arguments. 85 | for (int c = 0, index = 0; c != -1; index = 0) { 86 | c = getopt_long(argc, argv, Flag::codes().c_str(), options, &index); 87 | switch (c) { 88 | case -1: break; 89 | 90 | case 0: { // longopt with no short name. 91 | const string long_name = options[index].name; 92 | Flag * flag = Flag::long_names()[long_name]; 93 | if (flag) flag -> set(optarg); 94 | if (flag == &FLAGS_OPT_help) { 95 | Flag::show_help(cout); 96 | delete [] options; 97 | exit(0); 98 | } 99 | break; 100 | } 101 | 102 | case '?': { // unknown option. 103 | Flag::show_help(cerr); 104 | delete [] options; 105 | exit(1); 106 | } 107 | 108 | default: { // short option 109 | if (Flag::short_names().find(c) == Flag::short_names().end()) { 110 | // This should never happen. 111 | cerr << "ERROR: failed to find a flag matching '" << c << "'" << endl; 112 | } else { 113 | Flag * flag = Flag::short_names()[c]; 114 | if (flag) flag -> set(optarg); 115 | } 116 | break; 117 | } 118 | } 119 | } 120 | 121 | // Trim the non-argument params from argv. 122 | if (remove_flags) { 123 | for (*p_argc = 0; optind < argc; ++optind, ++(*p_argc)) { 124 | argv[*p_argc] = argv[optind]; 125 | } 126 | } 127 | 128 | delete [] options; 129 | return 0; 130 | } 131 | 132 | 133 | /** 134 | * Adds a flag to the map of flag names to values. Detects name collisions. 135 | */ 136 | template 137 | void safe_add(map & known, const T key, Flag * value) { 138 | if (known.find(key) != known.end()) { 139 | cerr << "ERROR: ambiguous flags defined: duplicate key: " 140 | << "'" << key << "'\n" << *known[key] << "\n" << *value << endl; 141 | } 142 | known[key] = value; 143 | } 144 | 145 | 146 | /** 147 | * Returns true if all characters in the input string are isgraph. 148 | */ 149 | bool all_isgraph(const char * input) { 150 | for (int i = 0; input && input[i] != '\0'; ++i) { 151 | if (!isgraph(input[i])) 152 | return false; 153 | } 154 | return true; 155 | } 156 | 157 | 158 | /** 159 | * Constructs a Flag object. 160 | * 161 | * Args: 162 | * ln: the long name of the flag (if any). 163 | * sn: a single character shor name of the flag (if any). 164 | * ds: the human-readable description of the flag. 165 | * ha: bool; true if this flag requires an argument, otherwise false. 166 | */ 167 | Flag::Flag(const char * ln, const char sn, const char * ds, const bool ha) 168 | : long_name(ln), short_name(sn), description(ds), has_arg(ha) 169 | { 170 | Flag::instances().push_back(this); 171 | 172 | // Map the names to this Flag object (if names are valid). 173 | if (short_name && isgraph(short_name)) 174 | safe_add(Flag::short_names(), short_name, this); 175 | if (all_isgraph(long_name)) { 176 | safe_add(Flag::long_names(), string(long_name), this); 177 | } else { 178 | cerr << "WARNING: Flag long name '" << long_name 179 | << "' is not legal and will be ignored." << endl; 180 | } 181 | 182 | // Keep track of the longest name for later help output formatting. 183 | string temp(long_name); 184 | if (has_arg) { 185 | temp += "=VALUE"; 186 | } 187 | if (temp.length() > longest_long_name) { 188 | longest_long_name = temp.length(); 189 | } 190 | 191 | // Create an option struct and add it to the list. 192 | struct option opt = {ln, has_arg ? 1 : 0, 0, sn}; 193 | Flag::options().push_back(opt); 194 | 195 | // Add the short_name to a flag_code string. 196 | if (short_name) { 197 | if (isgraph(short_name)) { 198 | Flag::codes().push_back(short_name); 199 | if (has_arg) { 200 | Flag::codes().push_back(':'); 201 | } 202 | } else { 203 | cerr << "WARNING: Flag short name character '" << short_name 204 | << "' is not legal and will be ignored." << endl; 205 | } 206 | } 207 | } 208 | 209 | 210 | /** 211 | * Destroys a Flag - this is required because there are virtual functions in 212 | * the class. 213 | */ 214 | Flag::~Flag() { 215 | // Nothing to do. 216 | } 217 | 218 | 219 | /** 220 | * Inserts this Flag into an ostream and then returns it. 221 | */ 222 | ostream & Flag::insert(ostream & out) const { 223 | if (short_name) 224 | out << " -" << short_name; 225 | else 226 | out << " "; 227 | 228 | if (long_name) { 229 | int padding = 2 + longest_long_name - string(long_name).length(); 230 | out << " --" << long_name; 231 | if (has_arg) { 232 | out << "=VALUE"; 233 | padding -= 6; 234 | } 235 | out << string(padding, ' '); 236 | } 237 | 238 | if (description) out << description; 239 | return out; 240 | } 241 | 242 | 243 | /** 244 | * Inserts a Flag into the ostrea. 245 | */ 246 | ostream & operator << (ostream & out, const Flag & flag) { 247 | return flag.insert(out); 248 | } 249 | 250 | 251 | /** 252 | * Initializes an IntFlag. 253 | */ 254 | IntFlag::IntFlag(const char * ln, const char sn, int * val, const int dv, const char * ds) 255 | : Flag(ln, sn, ds, true), value(val) 256 | { 257 | *value = dv; 258 | } 259 | 260 | 261 | /** 262 | * Sets the value of this IntFlag. 263 | * 264 | * Args: 265 | * optarg: the value to convert to an int using atoi. 266 | */ 267 | void IntFlag::set(const char * optarg) { 268 | *value = atoi(optarg); 269 | } 270 | 271 | 272 | /** 273 | * Inserts this IntFlag into an ostream. 274 | */ 275 | ostream & IntFlag::insert(ostream & out) const { 276 | Flag::insert(out); 277 | if (value && *value) { 278 | out << " Default: " << *value; 279 | } 280 | return out; 281 | } 282 | 283 | 284 | /** 285 | * Initialize a StringFlag. 286 | */ 287 | StringFlag::StringFlag(const char * ln, const char sn, string * val, const char * dv, const char * ds) 288 | : Flag(ln, sn, ds, true), value(val) 289 | { 290 | set(dv); 291 | } 292 | 293 | 294 | /** 295 | * Set the value of this StringFlag. 296 | */ 297 | void StringFlag::set(const char * optarg) { 298 | if (optarg) { 299 | *value = string(optarg); 300 | } else { 301 | value -> clear(); 302 | } 303 | } 304 | 305 | 306 | /** 307 | * Inserts this StringFlag into an ostream. 308 | */ 309 | ostream & StringFlag::insert(ostream & out) const { 310 | Flag::insert(out); 311 | if (!value -> empty()) { 312 | out << " Default: '" << *value << "'"; 313 | } 314 | return out; 315 | } 316 | 317 | 318 | /** 319 | * Initializes a BoolFlag. 320 | */ 321 | BoolFlag::BoolFlag(const char * ln, const char sn, bool * val, const bool dv, const char * ds, const bool has_arg) 322 | : Flag(ln, sn, ds, has_arg), value(val) 323 | { 324 | *val = dv; 325 | } 326 | 327 | 328 | /** 329 | * Sets the value of this flag by parsing the argument string. 330 | */ 331 | void BoolFlag::set(const char * optarg) { 332 | if (optarg) { 333 | string opt(optarg); 334 | if (opt == "true") { 335 | *value = true; 336 | } else if (opt == "false") { 337 | *value = false; 338 | } else { 339 | cerr << "ERROR: boolean flags must be either true or false. Got '" 340 | << optarg << "'" << endl; 341 | } 342 | } else { 343 | *value = true; 344 | } 345 | } 346 | 347 | 348 | /** 349 | * Inserts this BoolFlag into the argument ostream. 350 | */ 351 | ostream & BoolFlag::insert(ostream & out) const { 352 | Flag::insert(out); 353 | if (has_arg) { 354 | out << " Default: " << (*value ? "true" : "false"); 355 | } 356 | return out; 357 | } 358 | 359 | 360 | } // namespace flag 361 | -------------------------------------------------------------------------------- /src/flags.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | This class provides flags for programs, much in the same way that Google 19 | gflags does, although with fewer bells and whistles and no major system 20 | dependencies (only getopt). 21 | */ 22 | 23 | #ifndef __ASH_FLAGS__ 24 | #define __ASH_FLAGS__ 25 | 26 | #include /* for struct option */ 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | namespace flag { 34 | 35 | using std::ostream; 36 | using std::list; 37 | using std::map; 38 | using std::string; 39 | 40 | 41 | /** 42 | * Clients of this library define flags like this: 43 | * DEFINE_int(example, "E", -1, "An example flag."); 44 | * 45 | * This example defines an integer-value flag that can either be specified 46 | * on the command line by --example or -E. The default value is -1 and when 47 | * users invoke the command with the --help flag the "An example flag." 48 | * description is printed. 49 | * 50 | * Clients must also parse the main method argc and argv as follows: 51 | * Flag::parse(&argc, &argv, true); 52 | * 53 | * Clients then use this flag in code by referencing it as follows: 54 | * if (FLAG_example > 42) return; 55 | * 56 | * Clients wishing to use flags that don't require values should use the 57 | * DEFINE_flag macro. 58 | * 59 | * Clients wishing to not specify a single-character shortcut version should 60 | * use 0 instead of a quoted character. For example: 61 | * DEFINE_int(example, 0, -1, "An example flag (with no shortcut)."); 62 | * 63 | * Clients wishing to only specify a single-character shortcut should name 64 | * the flag the single-character they want. For example: 65 | * DEFINE_int(e, 0, -1, "An example single-character-only flag."); 66 | * 67 | * Clients wishing to have the flag-related parameters stripped from argv and 68 | * the count adjusted in argc should use the 'true' option for Flag::parse. 69 | * By leaving this false, the flag-related arguments are left in argv. 70 | */ 71 | 72 | #define DEFINE_int(long_name, short_name, default_val, desc) \ 73 | static int FLAGS_ ## long_name; \ 74 | static flag::IntFlag FLAGS_OPT_ ## long_name(#long_name, short_name, \ 75 | &FLAGS_ ## long_name, default_val, desc) 76 | 77 | #define DEFINE_string(long_name, short_name, default_val, desc) \ 78 | static string FLAGS_ ## long_name; \ 79 | static flag::StringFlag FLAGS_OPT_ ## long_name(#long_name, short_name, \ 80 | &FLAGS_ ## long_name, default_val, desc) 81 | 82 | #define DEFINE_bool(long_name, short_name, default_val, desc) \ 83 | static bool FLAGS_ ## long_name; \ 84 | static flag::BoolFlag FLAGS_OPT_ ## long_name(#long_name, short_name, \ 85 | &FLAGS_ ## long_name, default_val, desc, true) 86 | 87 | #define DEFINE_flag(long_name, short_name, desc) \ 88 | static bool FLAGS_ ## long_name; \ 89 | static flag::BoolFlag FLAGS_OPT_ ## long_name(#long_name, short_name, \ 90 | &FLAGS_ ## long_name, false, desc, false) 91 | 92 | #define STATIC_SINGLETON(name, type_name) \ 93 | static type_name & name() { \ 94 | static type_name rval; \ 95 | return rval; \ 96 | } 97 | 98 | /** 99 | * This class makes it easy to implement command-line flags. Class instances 100 | * are created by the preprocessor macros defined above. 101 | */ 102 | class Flag { 103 | // STATIC 104 | public: 105 | static int parse(int * argc, char *** argv, const bool remove_flags); 106 | static void show_help(ostream & out); 107 | 108 | private: 109 | STATIC_SINGLETON(codes, string) 110 | STATIC_SINGLETON(options, list) 111 | STATIC_SINGLETON(instances, list) 112 | // Note: these two maps do not use the STATIC_SINGLETON preprocessor macro 113 | // because the type name contains a comma; this confuses the parser. 114 | //STATIC_SINGLETON(short_names, map) 115 | //STATIC_SINGLETON(long_names, map) 116 | static map & short_names() { 117 | static map rval; 118 | return rval; 119 | } 120 | static map & long_names() { 121 | static map rval; 122 | return rval; 123 | } 124 | 125 | // NON-STATIC 126 | public: 127 | Flag(const char * long_name, const char short_name, const char * desc, 128 | const bool has_arg=false); 129 | virtual ~Flag(); 130 | 131 | virtual ostream & insert(ostream & out) const; 132 | 133 | virtual void set(const char * optarg) = 0; 134 | 135 | private: 136 | const char * long_name; 137 | const char short_name; 138 | const char * description; 139 | protected: 140 | const bool has_arg; 141 | 142 | // DISABLED 143 | private: 144 | Flag(const Flag & other); 145 | Flag & operator = (const Flag & other); 146 | }; 147 | 148 | 149 | /** 150 | * Inserts a Flag into an ostream. 151 | */ 152 | ostream & operator << (ostream & out, const Flag & flag); 153 | 154 | 155 | /** 156 | * A command-line flag containing an integer value. 157 | */ 158 | class IntFlag : public Flag { 159 | public: 160 | IntFlag(const char * ln, const char sn, int * val, const int dv, 161 | const char * ds); 162 | virtual ~IntFlag() {} 163 | virtual void set(const char * optarg); 164 | virtual ostream & insert(ostream & out) const; 165 | 166 | private: 167 | int * value; 168 | }; 169 | 170 | 171 | /** 172 | * A command-line flag containing a string value. 173 | */ 174 | class StringFlag : public Flag { 175 | public: 176 | StringFlag(const char * ln, const char sn, string * val, const char * dv, 177 | const char * ds); 178 | virtual ~StringFlag() {} 179 | virtual void set(const char * optarg); 180 | virtual ostream & insert(ostream & out) const; 181 | 182 | private: 183 | string * value; 184 | }; 185 | 186 | 187 | /** 188 | * A command-line flag containing a bool value. 189 | */ 190 | class BoolFlag : public Flag { 191 | public: 192 | BoolFlag(const char * ln, const char an, bool * val, const bool dv, 193 | const char * ds, const bool has_arg); 194 | virtual ~BoolFlag() {} 195 | virtual void set(const char * optarg); 196 | virtual ostream & insert(ostream & out) const; 197 | 198 | private: 199 | bool * value; 200 | }; 201 | 202 | } // namespace flag 203 | 204 | #endif /* __ASH_FLAGS__ */ 205 | -------------------------------------------------------------------------------- /src/formatter.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "formatter.hpp" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include "database.hpp" 24 | #include "logger.hpp" 25 | 26 | 27 | using namespace ash; 28 | using namespace std; 29 | 30 | 31 | /** 32 | * A mapping of name to singleton instance of all initialized Formatters. 33 | */ 34 | map Formatter::instances; 35 | 36 | 37 | /** 38 | * Returns the Formatter singleton matching the argument name, if found; 39 | * otherwise returns NULL. 40 | */ 41 | Formatter * Formatter::lookup(const string & name) { 42 | return instances.find(name) == instances.end() ? 0 : instances[name]; 43 | } 44 | 45 | 46 | /** 47 | * Returns a map of formatter names to descriptions. 48 | */ 49 | map Formatter::get_desc() { 50 | map rval; 51 | map::iterator i, e; 52 | 53 | for (i = instances.begin(), e = instances.end(); i != e; ++i) { 54 | if (i -> second) 55 | rval[i -> first] = i -> second -> description; 56 | } 57 | return rval; 58 | } 59 | 60 | 61 | /** 62 | * Creates a Formatter, making sure it has a unique name among all Formatters. 63 | */ 64 | Formatter::Formatter(const string & n, const string & d) 65 | : name(n), description(d), do_show_headings(true) 66 | { 67 | if (lookup(name)) { 68 | LOG(FATAL) << "Conflicting formatters declared: " << name; 69 | } 70 | instances[name] = this; 71 | } 72 | 73 | 74 | /** 75 | * Destroys this formatter. 76 | */ 77 | Formatter::~Formatter() { 78 | instances[name] = 0; 79 | } 80 | 81 | 82 | /** 83 | * Sets the internal state controlling whether headings are shown or not. 84 | */ 85 | void Formatter::show_headings(bool show) { 86 | do_show_headings = show; 87 | } 88 | 89 | 90 | /** 91 | * Makes this Formatter avaiable for use within the program. 92 | */ 93 | void SpacedFormatter::init() { 94 | static SpacedFormatter instance("aligned", 95 | "Columns are aligned and separated with spaces."); 96 | } 97 | 98 | 99 | /** 100 | * Returns the maximum widths required for each column in a result set. 101 | */ 102 | vector get_widths(const ResultSet * rs, bool do_show_headings) { 103 | vector widths; 104 | const size_t XX = 4; // The number of spaces between columns. 105 | 106 | // Initialize with the widths of the headings. 107 | size_t c = 0; 108 | ResultSet::HeadersType::const_iterator i, e; 109 | for (i = (rs -> headers).begin(), e = (rs -> headers).end(); i != e; ++i, ++c) 110 | if (do_show_headings) 111 | widths.push_back(XX + i -> size()); 112 | else 113 | widths.push_back(XX); 114 | 115 | // Limit the width of columns containing very wide elements. 116 | size_t max_w = 80; // TODO(cpa): make this a flag or configurable. 117 | 118 | // Loop ofer the rs.data looking for max column widths. 119 | for (size_t r = 0; r < rs -> rows; ++r) { 120 | for (size_t c = 0; c < rs -> columns; ++c) { 121 | widths[c] = max(widths[c], min(max_w, XX + (rs -> data[r][c]).size())); 122 | } 123 | } 124 | 125 | return widths; 126 | } 127 | 128 | 129 | /** 130 | * Calculates the ideal width for each column and inserts column data 131 | * left-aligned and separated by spaces. 132 | */ 133 | void SpacedFormatter::insert(const ResultSet * rs, ostream & out) const { 134 | if (!rs) return; // Sanity check. 135 | 136 | vector widths = get_widths(rs, do_show_headings); 137 | 138 | // Print the headings, if not suppressed. 139 | if (do_show_headings) { 140 | size_t c = 0, cols = widths.size(); 141 | ResultSet::HeadersType::const_iterator i, e; 142 | for (i = (rs -> headers).begin(), e = (rs -> headers).end(); i != e; ++i) { 143 | if (c < cols - 1) out << left << setw(widths[c++]); 144 | out << *i; 145 | } 146 | out << endl; 147 | } 148 | 149 | // Iterate over the data once more, printing. 150 | for (size_t r = 0; r < rs -> rows; ++r) { 151 | for (size_t c = 0; c < rs -> columns; ++c) { 152 | if (c < rs -> columns - 1) out << left << setw(widths[c]); 153 | out << (rs -> data)[r][c]; 154 | } 155 | out << endl; 156 | } 157 | } 158 | 159 | 160 | /** 161 | * Inserts a ResultSet with all values delimited by a common delimiter. 162 | */ 163 | void insert_delimited(const ResultSet * rs, ostream & out, const string & d, 164 | const bool do_show_headings) 165 | { 166 | if (!rs) return; 167 | 168 | const ResultSet::HeadersType & headers = rs -> headers; 169 | ResultSet::HeadersType::const_iterator i, e; 170 | 171 | if (do_show_headings) { 172 | size_t c = 0; 173 | for (i = headers.begin(), e = headers.end(); i != e; ++i, ++c) 174 | // Don't add a delimiter after the last column. 175 | out << *i << (c + 1 < rs -> columns ? d : ""); 176 | out << endl; 177 | } 178 | 179 | // Loop ofer the rs.data inserting delimited text. 180 | for (size_t r = 0; r < rs -> rows; ++r) { 181 | for (size_t c = 0; c < rs -> columns; ++c) { 182 | out << rs -> data[r][c] << (c + 1 < rs -> columns ? d : ""); 183 | } 184 | out << endl; 185 | } 186 | } 187 | 188 | 189 | /** 190 | * Makes this Formatter avaiable for use within the program. 191 | */ 192 | void CsvFormatter::init() { 193 | static CsvFormatter instance("csv", 194 | "Columns are comma separated with strings quoted."); 195 | } 196 | 197 | 198 | /** 199 | * Inserts data separated by commas. 200 | */ 201 | void CsvFormatter::insert(const ResultSet * rs, ostream & out) const { 202 | insert_delimited(rs, out, ",", do_show_headings); 203 | } 204 | 205 | 206 | /** 207 | * Makes this Formatter avaiable for use within the program. 208 | */ 209 | void NullFormatter::init() { 210 | static NullFormatter instance("null", 211 | "Columns are null separated with strings quoted."); 212 | } 213 | 214 | 215 | /** 216 | * Inserts data separated by \0 characters. 217 | */ 218 | void NullFormatter::insert(const ResultSet * rs, ostream & out) const { 219 | insert_delimited(rs, out, string("\0", 1), do_show_headings); 220 | } 221 | 222 | 223 | /** 224 | * Makes this Formatter avaiable for use within the program. 225 | */ 226 | void GroupedFormatter::init() { 227 | static GroupedFormatter instance("auto", 228 | "Automatically group redundant values."); 229 | } 230 | 231 | 232 | /** 233 | * Determines how many levels should be auto-grouped. 234 | */ 235 | int get_grouped_level_count(const ResultSet * rs, const vector & widths) 236 | { 237 | if (!rs) return 0; // Sanity check. 238 | 239 | size_t width = 0, length = rs -> rows, XX = 4; 240 | for (size_t i = 0, e = widths.size(); i != e; ++i) width += widths[i]; 241 | size_t min_area = length * width; 242 | 243 | // examine the columns from left to right simulating how much screen space 244 | // would be saved by grouping that column. If there is a net reduction in 245 | // screen 'area', then the column will be grouped. Otherwise it will not. 246 | 247 | // store area of output after simulating grouping at each level successively. 248 | // the rightmost minimum area will be chosen. 249 | // 250 | // For example, consider the following areas after simulating grouping: 251 | // areas = [100, 90, 92, 90, 140, 281] 252 | // 253 | // With 1 level of grouping and with 3 levels of grouping we get the same 254 | // screen area, however the rightmost value is chosen, so the return value 255 | // will be 3. 256 | vector areas(widths.size(), width * length); 257 | 258 | string prev; 259 | for (size_t c = 0, cols = rs -> columns; c < cols; ++c) { 260 | // test each row in the column to see if it is a duplicate of the previous 261 | // row. If so, it will be de-duped in the output. If not, it means an 262 | // extra row will be added, so we adjust the new_len variable accordingly. 263 | prev = ""; 264 | for (size_t r = 0, rows = rs -> rows; r < rows; ++r) { 265 | if (prev != rs -> data[r][c]) { 266 | ++length; 267 | prev = rs -> data[r][c]; 268 | } 269 | } 270 | // to calculate the new width, we need to consider both the width of the 271 | // grouped column and the width of the remaining columns. we also need to 272 | // consider the width of the indent. 273 | width = max(width - widths[c], widths[c]) + XX * (c + 1); 274 | min_area = min(length * width, min_area); 275 | if (c < rs -> columns - 1) areas[c + 1] = width * length; 276 | } 277 | // Find the rightmost minimum area from all simulated areas. 278 | for (size_t c = rs -> columns; c > 0; --c) { 279 | if (areas[c - 1] == min_area) return c - 1; 280 | } 281 | return 0; 282 | } 283 | 284 | 285 | /** 286 | * Inserts auto-grouped history, starting with the leftmost columns. 287 | */ 288 | void GroupedFormatter::insert(const ResultSet * rs, ostream & out) const { 289 | if (!rs) return; // Sanity check. 290 | 291 | vector widths = get_widths(rs, do_show_headings); 292 | size_t levels = get_grouped_level_count(rs, widths); 293 | 294 | if (do_show_headings) { 295 | ResultSet::HeadersType::const_iterator h = rs -> headers.begin(); 296 | for (size_t c = 0, cols = rs -> columns; c < cols; ++c) { 297 | if (c < levels) { 298 | // if it's a grouped column, print it followed by a newline and an 299 | // indent for the next line. 300 | out << *h << "\n"; 301 | for (size_t i = c + 1; i > 0; --i) out << " "; 302 | } else { 303 | // if it's not the last column, we set the alignment left and pad the 304 | // value with spaces. Otherwise we just print the value to remove 305 | // trailing spaces from the last value. 306 | if (c < cols - 1) { 307 | out << left << setw(widths[c]) << *h; 308 | } else { 309 | out << *h; 310 | } 311 | } 312 | ++h; 313 | } 314 | out << endl; 315 | } 316 | 317 | vector prev(levels); 318 | string value; 319 | for (size_t r = 0, rows = rs -> rows; r < rows; ++r) { 320 | for (size_t c = 0, cols = rs -> columns; c < cols; ++c) { 321 | value = rs -> data[r][c]; 322 | if (c < levels) { 323 | if (value != prev[c] || r == 0) { 324 | // The value has not been grouped, 325 | out << value; 326 | if (c < cols - 1) { 327 | // Since it's not the final column, wrap the line and indent 328 | // to the next level in preparation for the next value. 329 | out << "\n"; 330 | for (size_t i = c + 1; i > 0; --i) out << " "; 331 | for (size_t i = c; i < levels; ++i) prev[i] = ""; 332 | } 333 | prev[c] = value; 334 | } else { 335 | // The value has been grouped, only print the indent. 336 | out << " "; 337 | } 338 | } else { 339 | // Normal (non-grouped) case. 340 | if (c < cols - 1) { 341 | out << left << setw(widths[c]) << value; 342 | } else { 343 | out << value; 344 | } 345 | } 346 | } 347 | out << endl; 348 | } 349 | } 350 | 351 | -------------------------------------------------------------------------------- /src/formatter.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * This class provides a base and some concrete examples of classes that are 19 | * designed to take a DB ResultSet and output the contents in a formatted way. 20 | */ 21 | 22 | #ifndef __ASH_FORMATTER__ 23 | #define __ASH_FORMATTER__ 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | namespace ash { 30 | 31 | using std::map; 32 | using std::ostream; 33 | using std::string; 34 | 35 | class ResultSet; // forward declaration. 36 | 37 | 38 | /** 39 | * Abstract base class for an object that inserts a ResultSet into an ostream. 40 | */ 41 | class Formatter { 42 | // STATIC: 43 | public: 44 | static Formatter * lookup(const string & name); 45 | static map get_desc(); 46 | 47 | private: 48 | static map instances; 49 | 50 | // NON-STATIC: 51 | public: 52 | virtual ~Formatter(); 53 | 54 | virtual void insert(const ResultSet * rs, ostream & out) const = 0; 55 | 56 | void show_headings(bool show); 57 | 58 | protected: 59 | Formatter(const string & name, const string & description); 60 | 61 | protected: 62 | const string name, description; 63 | bool do_show_headings; 64 | 65 | // DISALLOWED: 66 | private: 67 | Formatter(const Formatter & other); 68 | Formatter & operator = (const Formatter & other); 69 | }; 70 | 71 | 72 | /** 73 | * This is a convenience macro intended to be used only in this header. 74 | * It defines a class that must implement a static init and non-static insert. 75 | */ 76 | #define FORMATTER(NAME) \ 77 | class NAME ## Formatter : public Formatter { \ 78 | public: \ 79 | static void init(); \ 80 | \ 81 | public: \ 82 | virtual ~NAME ## Formatter() {} \ 83 | virtual void insert(const ResultSet * rs, ostream & out) const; \ 84 | \ 85 | protected: \ 86 | NAME ## Formatter(const string & name, const string & description) \ 87 | : Formatter(name, description) {} \ 88 | } 89 | 90 | 91 | /** 92 | * Singleton class that converts a result set into output, spacing all columns 93 | * evenly using spaces to align columns. Each column is as wide as its widest 94 | * element. 95 | */ 96 | FORMATTER(Spaced); 97 | 98 | 99 | /** 100 | * Singleton class that converts a result set into output, delimiting all 101 | * columns with commas. 102 | */ 103 | FORMATTER(Csv); 104 | 105 | 106 | /** 107 | * Singleton class that converts a result set into output, collapsing repeated 108 | * values on the left edge if the net result saves printed space. 109 | */ 110 | FORMATTER(Grouped); 111 | 112 | 113 | /** 114 | * Singleton class that converts a result set into output, delimiting all 115 | * columns with \0 characters (NULL). 116 | */ 117 | FORMATTER(Null); 118 | 119 | 120 | } // namespace ash 121 | 122 | #endif /* __ASH_FORMATTER__ */ 123 | -------------------------------------------------------------------------------- /src/logger.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "logger.hpp" 18 | 19 | #include "config.hpp" 20 | 21 | #include /* for exit */ 22 | #include /* for time, strftime, localtime */ 23 | 24 | #include 25 | 26 | using namespace ash; 27 | using namespace std; 28 | 29 | 30 | /** 31 | * Converts a string representation of the enum value to the enum code. 32 | */ 33 | const Severity parse(const string & input) { 34 | if (input == "DEBUG") return DEBUG; 35 | if (input == "INFO") return INFO; 36 | if (input == "WARNING") return WARNING; 37 | if (input == "ERROR") return ERROR; 38 | if (input == "FATAL") return FATAL; 39 | return UNKNOWN; 40 | } 41 | 42 | 43 | /** 44 | * Converts a severity level to a string. 45 | */ 46 | const char * to_str(const Severity level) { 47 | switch (level) { 48 | case DEBUG: return "DEBUG"; 49 | case INFO: return "INFO"; 50 | case WARNING: return "WARNING"; 51 | case ERROR: return "ERROR"; 52 | case FATAL: return "FATAL"; 53 | case UNKNOWN: // fallthrough. 54 | default: return "UNKNOWN"; 55 | } 56 | } 57 | 58 | 59 | /** 60 | * Returns the log file filename designated as the target for a given severity 61 | * level. This relies on environment variables ASH_CFG_LOG_LEVEL and 62 | * ASH_CFG_LOG_FILE to be populated or it will return /dev/null as a default. 63 | */ 64 | string get_target(const Severity level) { 65 | Config & config = Config::instance(); 66 | 67 | // Default to /dev/null if the visibility is too low for this Logger. 68 | const Severity visible = parse(config.get_string("LOG_LEVEL", "DEBUG")); 69 | if (level < visible) return "/dev/null"; 70 | // Use the configured target file. 71 | return config.get_string("LOG_FILE", "/dev/null"); 72 | } 73 | 74 | 75 | /** 76 | * Constructs a logger and adds the severity to the output. 77 | */ 78 | Logger::Logger(const Severity lvl) 79 | : ostream(NULL), log(get_target(lvl).c_str(), fstream::out | fstream::app), level(lvl) 80 | { 81 | char time_now[200]; 82 | time_t t = time(NULL); 83 | 84 | // Get the session_id, if it has already beeen set. 85 | const char * session_id = 86 | getenv("ASH_SESSION_ID") ? getenv("ASH_SESSION_ID") : "?"; 87 | 88 | // Get the time now. 89 | struct tm * tmp = localtime(&t); 90 | if (tmp == NULL) { 91 | perror("advanced shell history Logger: localtime"); 92 | if (level != FATAL) { 93 | LOG(FATAL) << "Failed to get localtime on this machine."; // recurse 94 | } else { 95 | log << "SESSION " << session_id << ": " << to_str(level) << ": "; 96 | return; 97 | } 98 | } 99 | 100 | // Get the log date format, if one was specified. 101 | Config & config = Config::instance(); 102 | string format = config.get_string("LOG_DATE_FMT", "%Y-%m-%d %H:%M:%S %Z: "); 103 | if (strftime(time_now, sizeof(time_now), format.c_str(), tmp) == 0) { 104 | if (level != FATAL) { // avoids infinite recursion. 105 | LOG(FATAL) << "ASH_CFG_LOG_DATE_FMT is invalid: '" << format << "'"; 106 | } else { 107 | log << "SESSION " << session_id << ": " << to_str(level) << ": "; 108 | } 109 | } else { 110 | log << time_now << "SESSION " << session_id << ": " << to_str(level) 111 | << ": "; 112 | } 113 | } 114 | 115 | 116 | /** 117 | * Destroys a Logger, flushing output and exiting the program if the severity 118 | * level was FATAL. 119 | */ 120 | Logger::~Logger() { 121 | log << endl; 122 | log.close(); 123 | if (level == FATAL) exit(1); 124 | } 125 | 126 | -------------------------------------------------------------------------------- /src/logger.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | /* 17 | 18 | This utility provides a simple logger complete with various levels of 19 | visibility. This is essentially a small subset interface of the Google 20 | logging library (glog) with no extra dependencies. 21 | 22 | */ 23 | #ifndef __ASH_LOGGER__ 24 | #define __ASH_LOGGER__ 25 | 26 | #include 27 | using std::ostream; 28 | using std::ofstream; 29 | 30 | namespace ash { 31 | 32 | 33 | /** 34 | * The severity levels to which logged messages can be set. This allows 35 | * users to easily add logged messages that are intended only to be seen 36 | * while debugging. 37 | */ 38 | enum Severity { 39 | DEBUG, 40 | INFO, 41 | WARNING, 42 | ERROR, 43 | FATAL, 44 | UNKNOWN 45 | }; 46 | 47 | 48 | /** 49 | * This class extends the ostream base to take advantage of predefined 50 | * templates while internally passing input to an ofstream which writes to 51 | * a designated log file. 52 | */ 53 | class Logger : public ostream { 54 | public: 55 | Logger(const Severity level); 56 | ~Logger(); 57 | 58 | /** 59 | * This templated method simply hands-off insertion to the underlying 60 | * ofstream. 61 | */ 62 | template 63 | ostream & operator << (insertable something) { 64 | return log << something; 65 | } 66 | 67 | /** 68 | * This is needed to handle the special case where a stream manipulator 69 | * is inserted into a Logger first. For example: LOG(INFO) << endl; 70 | */ 71 | typedef std::basic_ostream > StreamType; 72 | typedef StreamType & (*Manipulator) (StreamType &); 73 | ostream & operator << (const Manipulator & manipulate) { 74 | return manipulate(log); 75 | } 76 | 77 | private: 78 | ofstream log; 79 | Severity level; 80 | 81 | // DISALLOWED: 82 | private: 83 | Logger(const Logger & other); 84 | Logger & operator = (const Logger & other); 85 | }; 86 | 87 | 88 | /** 89 | * This macro allows users to use LOG(DEBUG) instead of Logger(DEBUG). 90 | */ 91 | #ifndef LOG 92 | #define LOG(level) (Logger(level)) 93 | #endif 94 | 95 | 96 | } // namespace: ash 97 | 98 | #endif /* __ASH_LOGGER__ */ 99 | -------------------------------------------------------------------------------- /src/queries.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef __ASH_QUERIES__ 17 | #define __ASH_QUERIES__ 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | namespace ash { 24 | 25 | 26 | using std::list; 27 | using std::map; 28 | using std::string; 29 | 30 | 31 | /** 32 | * A container for all queries specified in /etc/ash/queries or in the user 33 | * defined ~/.ash/queries file. 34 | */ 35 | class Queries { 36 | public: 37 | static void add(string & name, string & desc, string & sql); 38 | static bool has(const string & name); 39 | 40 | static list get_names(); 41 | 42 | static map get_desc(); 43 | static string get_desc(const string & name); 44 | 45 | static map get_sql(); 46 | static string get_raw_sql(const string & name); 47 | static string get_sql(const string & name); 48 | 49 | private: 50 | static void lazy_load(); 51 | 52 | private: 53 | static map descriptions; 54 | static map queries; 55 | 56 | private: 57 | Queries(); 58 | }; 59 | 60 | 61 | } // namespace ash 62 | 63 | #endif // __ASH_QUERIES__ 64 | 65 | -------------------------------------------------------------------------------- /src/queries.l: -------------------------------------------------------------------------------- 1 | %{ 2 | /* 3 | Copyright 2011 Carl Anderson 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | #include "queries.hpp" 18 | 19 | #include "logger.hpp" 20 | 21 | #include 22 | #include 23 | 24 | 25 | // This is implemented at the end of this file, after the syntax definition. 26 | extern int yylex(); 27 | extern "C" int yywrap(); 28 | 29 | 30 | namespace ash { 31 | using namespace std; 32 | 33 | namespace query { 34 | 35 | // Temp variables for parsing. 36 | string * name, * desc, * sql; 37 | int braces = 0; 38 | stringstream * ss; 39 | 40 | // The config files to parse for queries. 41 | list files; 42 | 43 | // The file currently being parsed. 44 | string parsing; 45 | 46 | } // namespace query 47 | 48 | 49 | map Queries::descriptions; 50 | map Queries::queries; 51 | 52 | 53 | /** 54 | * Add a query (and description) to the collection of saved queries. 55 | */ 56 | void Queries::add(string & name, string & desc, string & sql) { 57 | Queries::descriptions[name] = desc; 58 | Queries::queries[name] = sql; 59 | } 60 | 61 | 62 | /** 63 | * Get the set of names and descriptions of all queries. 64 | */ 65 | map Queries::get_desc() { 66 | Queries::lazy_load(); 67 | return Queries::descriptions; 68 | } 69 | 70 | 71 | /** 72 | * Return true if the argument query is already defined. 73 | */ 74 | bool Queries::has(const string & name) { 75 | Queries::lazy_load(); 76 | return 77 | Queries::queries.find(name) != Queries::queries.end() && 78 | Queries::descriptions.find(name) != Queries::descriptions.end(); 79 | } 80 | 81 | 82 | /** 83 | * Return the description of an argument query. 84 | */ 85 | string Queries::get_desc(const string & name) { 86 | Queries::lazy_load(); 87 | return Queries::has(name) ? Queries::descriptions[name] : ""; 88 | } 89 | 90 | 91 | /** 92 | * Return the set of names and SQL for all saved queries. 93 | */ 94 | map Queries::get_sql() { 95 | Queries::lazy_load(); 96 | return Queries::queries; 97 | } 98 | 99 | 100 | /** 101 | * Return the query without substituting environment variables. 102 | */ 103 | string Queries::get_raw_sql(const string & name) { 104 | Queries::lazy_load(); 105 | return Queries::has(name) ? Queries::queries[name] : ""; 106 | } 107 | 108 | 109 | /** 110 | * Returns the stored query, substituting variables with current values within 111 | * the query string. 112 | */ 113 | string Queries::get_sql(const string & name) { 114 | Queries::lazy_load(); 115 | 116 | // Sanity check, do nothing if the requested query is not found. 117 | if (!Queries::has(name)) return ""; 118 | 119 | LOG(DEBUG) << "Fetching query: '" << Queries::queries[name] << "'"; 120 | 121 | // Eval the query, to expand any and all referenced variables. 122 | stringstream ss; 123 | ss << "cat <[ \t\n]+ ; // WHITESPACE 242 | #.*\n ; // # LINE COMMENT. 243 | [a-zA-Z_0-9-]+ { 244 | ash::query::desc = ash::query::sql = 0; 245 | ash::query::name = new std::string(yytext); 246 | BEGIN(Q1); 247 | } 248 | 249 | /* State Q1 - Read a queary name, expecting a COLON. */ 250 | #.* ; // LINE COMMENT. 251 | [ \t\n]+ ; // WHITESPACE 252 | ":" BEGIN(Q2); 253 | [^#: \t\n]+ ash::expected(":"); 254 | 255 | 256 | /* State Q2 - Read a query name and COLON, expecting an LBRACE. */ 257 | #.* ; // LINE COMMENT. 258 | [ \t\n]+ ; // WHITESPACE 259 | "{" BEGIN(QUERY); 260 | [^#: \t\n]+ ash::expected("{"); 261 | 262 | 263 | /* State QUERY - Expecting a description and sql definition. */ 264 | #.* ; // LINE COMMENT. 265 | [ \t\n]+ ; // WHITESPACE 266 | "description" { 267 | if (ash::query::desc) 268 | ash::fail("multiple descriptions defined"); 269 | BEGIN(D1); 270 | } 271 | "sql" { 272 | if (ash::query::sql) 273 | ash::fail("multiple sql sections defined"); 274 | BEGIN(SQL); 275 | } 276 | "}" { 277 | using namespace ash; 278 | using namespace ash::query; 279 | 280 | // These checks should never be needed. 281 | if (!name) expected("a query name for the query."); 282 | if (!desc) expected("a description in the query."); 283 | if (!sql) expected("a sql field in the query."); 284 | 285 | Queries::add(*name, *desc, *sql); 286 | 287 | // Clean up for the next query to be parsed. 288 | delete name; delete desc; delete sql; 289 | name = desc = sql = 0; 290 | BEGIN(INITIAL); 291 | } 292 | 293 | /* State D1 - Read keyword 'description', expecting a COLON. */ 294 | #.* ; // LINE COMMENT. 295 | [ \t\n]+ ; // WHITESPACE 296 | ":" BEGIN(DESC); 297 | [^#: \t\n]+ ash::expected(":"); 298 | 299 | /* State DESC - Read 'description:' - expecting a quoted string. */ 300 | #.* ; // LINE COMMENT. 301 | [ \t\n]+ ; // WHITESPACE 302 | \" BEGIN(STR); 303 | [^# \t\n\"] ash::expected("\""); 304 | 305 | /* State STR - Read a quoted string. */ 306 | [^"\n]*\" { 307 | ash::query::desc = new std::string(yytext, yyleng-1); 308 | BEGIN(QUERY); 309 | } 310 | \n ash::expected("\" - Multi-line strings are illegal."); 311 | 312 | /* State SQL - read 'sql' token, expecting a COLON. */ 313 | #.* ; // LINE COMMENT. 314 | [ \t\n]+ ; // WHITESPACE 315 | ":" BEGIN(SQL1); 316 | 317 | /* State SQL1 - read 'sql:' token, expecting a LEFT_BRACE. */ 318 | #.* ; // LINE COMMENT. 319 | [ \t\n]+ ; // WHITESPACE 320 | \{[ \t]* { 321 | ash::query::ss = new std::stringstream(); 322 | BEGIN(SQL2); 323 | } 324 | . ash::expected("{"); 325 | 326 | /* State SQL2 - read 'sql: {' token, expecting a closing RBRACE */ 327 | [^{}]+ *ash::query::ss << yytext; 328 | "{" { 329 | ++ash::query::braces; 330 | *ash::query::ss << "{"; 331 | } 332 | "}" { 333 | using namespace ash::query; 334 | if (braces) { 335 | --braces; 336 | *ss << "}"; 337 | } else { 338 | sql = new std::string(ss -> str()); 339 | delete ss; 340 | ss = 0; 341 | BEGIN(QUERY); 342 | } 343 | } 344 | 345 | /* FAIL BUCKET - this matches any character that is not covered above. */ 346 | . { 347 | ash::fail() << ": Unexpected character." << std::endl; 348 | exit(1); 349 | } 350 | %% 351 | 352 | 353 | /** 354 | * This method is called by flex whenever the yyin stream is closed. This 355 | * gives us a chance to change the source file and resume parsing. 356 | */ 357 | int yywrap() { 358 | using namespace ash; 359 | using namespace std; 360 | 361 | // Reset line number, since we're switching to a new file. 362 | yyset_lineno(1); 363 | 364 | // Close the old file, if it's actually open. 365 | if (yyin != 0) fclose(yyin); 366 | 367 | while (!query::files.empty()) { 368 | query::parsing = query::files.front(); 369 | query::files.pop_front(); 370 | yyin = fopen(query::parsing.c_str(), "r"); 371 | if (yyin) return 0; 372 | LOG(DEBUG) << "File could not be opened: " << query::parsing; 373 | } 374 | LOG(DEBUG) << "Done parsing config files."; 375 | return 1; 376 | } 377 | 378 | -------------------------------------------------------------------------------- /src/session.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "session.hpp" 18 | 19 | #include "unix.hpp" 20 | 21 | #include 22 | 23 | 24 | using namespace ash; 25 | using namespace std; 26 | 27 | 28 | /** 29 | * Registers this table for use in the Database. 30 | */ 31 | void Session::register_table() { 32 | string name = "sessions"; 33 | stringstream ss; 34 | ss << "CREATE TABLE IF NOT EXISTS " << name << " ( \n" 35 | << " id integer primary key autoincrement, \n" 36 | << " hostname varchar(128), \n" 37 | << " host_ip varchar(40), \n" 38 | << " ppid int(5) not null, \n" 39 | << " pid int(5) not null, \n" 40 | << " time_zone str(3) not null, \n" 41 | << " start_time integer not null, \n" 42 | << " end_time integer, \n" 43 | << " duration integer, \n" 44 | << " tty varchar(20) not null, \n" 45 | << " uid int(16) not null, \n" 46 | << " euid int(16) not null, \n" 47 | << " logname varchar(48), \n" 48 | << " shell varchar(50) not null, \n" 49 | << " sudo_user varchar(48), \n" 50 | << " sudo_uid int(16), \n" 51 | << " ssh_client varchar(60), \n" 52 | << " ssh_connection varchar(100) \n" 53 | << ");"; 54 | DBObject::register_table(name, ss.str()); 55 | } 56 | 57 | 58 | /** 59 | * Initialize a Session object. 60 | */ 61 | Session::Session() { 62 | values["time_zone"] = unix::time_zone(); 63 | values["start_time"] = unix::time(); 64 | values["ppid"] = unix::ppid(); 65 | values["pid"] = unix::pid(); 66 | values["tty"] = unix::tty(); 67 | values["uid"] = unix::uid(); 68 | values["euid"] = unix::euid(); 69 | values["logname"] = unix::login_name(); 70 | values["hostname"] = unix::host_name(); 71 | values["host_ip"] = unix::host_ip(); 72 | values["shell"] = unix::shell(); 73 | values["sudo_user"] = unix::env("SUDO_USER"); 74 | values["sudo_uid"] = unix::env("SUDO_UID"); 75 | values["ssh_client"] = unix::env("SSH_CLIENT"); 76 | values["ssh_connection"] = unix::env("SSH_CONNECTION"); 77 | } 78 | 79 | 80 | /** 81 | * This is required since it was declared virtual in the base class. 82 | */ 83 | Session::~Session() { 84 | // Nothing to do here. 85 | } 86 | 87 | 88 | /** 89 | * Return the name of the table backing this class. 90 | */ 91 | const string Session::get_name() const { 92 | return "sessions"; 93 | } 94 | 95 | 96 | /** 97 | * Returns a query to finalize this Session in the sessions table. 98 | */ 99 | const string Session::get_close_session_sql() const { 100 | stringstream ss; 101 | ss << "UPDATE sessions \n" 102 | << "SET \n" 103 | << " end_time = " << unix::time() << ", \n" 104 | << " duration = " << unix::time() << " - start_time \n" 105 | << "WHERE id == " << unix::env("ASH_SESSION_ID") << "; "; 106 | return ss.str(); 107 | } 108 | -------------------------------------------------------------------------------- /src/session.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __ASH_SESSION__ 18 | #define __ASH_SESSION__ 19 | 20 | #include 21 | #include 22 | 23 | #include "database.hpp" 24 | 25 | using std::map; 26 | using std::string; 27 | 28 | namespace ash { 29 | 30 | 31 | /** 32 | * This class encapsulates session-specific data. 33 | */ 34 | class Session : public DBObject { 35 | public: 36 | static void register_table(); 37 | 38 | public: 39 | Session(); 40 | virtual ~Session(); 41 | 42 | virtual const string get_close_session_sql() const; 43 | virtual const string get_name() const; 44 | }; 45 | 46 | 47 | } // namespace ash 48 | 49 | #endif /* __ASH_SESSION__ */ 50 | 51 | -------------------------------------------------------------------------------- /src/unix.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "unix.hpp" 18 | 19 | #include "config.hpp" 20 | #include "database.hpp" 21 | #include "logger.hpp" 22 | #include "util.hpp" 23 | 24 | #include /* for ifstream */ 25 | #include /* for stringstream */ 26 | 27 | #include /* for inet_ntop */ 28 | #include /* for errno */ 29 | #include /* for getifaddrs, freeifaddrs */ 30 | #include /* for getenv */ 31 | #include /* for strlen */ 32 | #include 33 | #include 34 | #include /* for time */ 35 | #include /* for getppid, get_current_dir_name */ 36 | 37 | 38 | using namespace ash; 39 | using namespace std; 40 | 41 | 42 | /** 43 | * Returns the i'th value (target) of /proc/${pid}/stat. 44 | */ 45 | const string proc_stat(const int target, const pid_t pid) { 46 | stringstream ss; 47 | ss << "/proc/" << pid << "/stat"; 48 | ifstream fin(ss.str().c_str()); 49 | string token; 50 | for (int i = 0; fin.good(); ++i) { 51 | fin >> token; 52 | if (i == target) break; 53 | } 54 | fin.close(); 55 | return token; 56 | } 57 | 58 | 59 | /** 60 | * Returns the current working directory. 61 | */ 62 | const string unix::cwd() { 63 | char * c = get_current_dir_name(); 64 | if (!c) return DBObject::quote(0); 65 | string cwd(c); 66 | free(c); 67 | return DBObject::quote(cwd); 68 | } 69 | 70 | 71 | /** 72 | * Returns true if the argument file exists. 73 | */ 74 | bool exists(const char * dir) { 75 | struct stat st; 76 | if (stat(dir, &st) != 0) { 77 | LOG(DEBUG) << "tested file does not exist: '" << dir << "': " 78 | << strerror(errno); 79 | return false; 80 | } 81 | return true; 82 | } 83 | 84 | 85 | /** 86 | * Returns the output of the ps command (minus trailing newlines). 87 | */ 88 | const char * ps(const string & args, pid_t pid) { 89 | LOG(DEBUG) << "looking at ps output for ps " << args << " " << pid; 90 | stringstream ss; 91 | ss << "/bin/ps " << args << " " << pid; 92 | FILE * p = popen(ss.str().c_str(), "r"); 93 | if (p) { 94 | char buffer[256]; 95 | char * rval = fgets(buffer, 256, p); 96 | pclose(p); 97 | if (!rval) return "null"; 98 | for (size_t i = strlen(rval) - 1; i > 0; --i) 99 | if (rval[i] == '\n') { 100 | rval[i] = '\0'; 101 | break; 102 | } 103 | return rval; 104 | } 105 | return "null"; 106 | } 107 | 108 | 109 | /** 110 | * Returns the parent process id of the argument process id. 111 | */ 112 | const pid_t get_ppid(const pid_t pid) { 113 | return atoi(exists("/proc") ? proc_stat(3, pid).c_str() : ps("ho ppid", pid)); 114 | } 115 | 116 | 117 | /** 118 | * Returns the pid of the command-line shell. 119 | */ 120 | const pid_t shell_pid() { 121 | return get_ppid(getppid()); 122 | } 123 | 124 | 125 | /** 126 | * Returns the parent process ID of the shell process. 127 | */ 128 | const string unix::ppid() { 129 | stringstream ss; 130 | ss << get_ppid(shell_pid()); 131 | return ss.str(); 132 | } 133 | 134 | 135 | /** 136 | * Returns the name of the running shell. 137 | */ 138 | const string unix::shell() { 139 | if (exists("/proc")) { 140 | LOG(DEBUG) << "looking for shell name in /proc/" << shell_pid() << "/stat."; 141 | string sh = proc_stat(1, shell_pid()); 142 | // If the shell name is wrapped in parentheses, strip them. 143 | if (!sh.empty() && sh[0] == '(' && sh[sh.length() - 1] == ')') { 144 | sh = sh.substr(1, sh.length() - 2); 145 | } 146 | return DBObject::quote(sh); 147 | } else { 148 | // This is expected on OSX - no procfs so no /proc directory. 149 | string sh = ps("ho command", shell_pid()); 150 | return sh == "null" ? sh : DBObject::quote(sh); 151 | } 152 | return "null"; 153 | } 154 | 155 | 156 | /** 157 | * Returns the effective user ID. 158 | */ 159 | const string unix::euid() { 160 | return Util::to_string(geteuid()); 161 | } 162 | 163 | 164 | /** 165 | * Returns the process ID of the shell. 166 | */ 167 | const string unix::pid() { 168 | return Util::to_string(shell_pid()); 169 | } 170 | 171 | 172 | /** 173 | * Returns the current local UNIX epoch timestamp. 174 | */ 175 | const string unix::time() { 176 | return Util::to_string(::time(0)); 177 | } 178 | 179 | 180 | /** 181 | * Returns the local time zone code. 182 | */ 183 | const string unix::time_zone() { 184 | stringstream ss; 185 | char zone_buffer[5]; 186 | time_t now = ::time(0); 187 | strftime(zone_buffer, 5, "%Z", localtime(&now)); 188 | ss << zone_buffer; 189 | return DBObject::quote(ss.str()); 190 | } 191 | 192 | 193 | /** 194 | * Returns the user ID running the command. 195 | */ 196 | const string unix::uid() { 197 | return Util::to_string(getuid()); 198 | } 199 | 200 | 201 | /** 202 | * Returns a list of IP addresses owned on the machine running the commands. 203 | */ 204 | const string unix::host_ip() { 205 | struct ifaddrs * addrs; 206 | if (getifaddrs(&addrs)) { 207 | LOG(INFO) << "No network addresses detected."; 208 | return "null"; 209 | } 210 | 211 | Config & config = Config::instance(); 212 | bool skip_lo = config.sets("SKIP_LOOPBACK"); 213 | 214 | int ips = 0; 215 | stringstream ss; 216 | char buffer[256]; 217 | for (struct ifaddrs * i = addrs; i; i = i -> ifa_next) { 218 | struct sockaddr * address = i -> ifa_addr; 219 | if (address == NULL) { 220 | LOG(WARNING) << "Skipped a null network address."; 221 | continue; 222 | } 223 | if (skip_lo && i -> ifa_name && string("lo") == i -> ifa_name) { 224 | LOG(DEBUG) << "Skipped a loopback address, as configured."; 225 | continue; 226 | } 227 | 228 | sa_family_t family = address -> sa_family; 229 | switch (family) { 230 | case AF_INET: { 231 | if (config.sets("LOG_IPV4")) { 232 | struct sockaddr_in * a = (struct sockaddr_in *) address; 233 | inet_ntop(family, &(a -> sin_addr), buffer, sizeof(buffer)); 234 | } else { 235 | LOG(DEBUG) << "Skipped an IPv4 address for: " << i -> ifa_name; 236 | } 237 | break; 238 | } 239 | case AF_INET6: { 240 | if (config.sets("LOG_IPV6")) { 241 | struct sockaddr_in6 * a = (struct sockaddr_in6 *) address; 242 | inet_ntop(family, &(a -> sin6_addr), buffer, sizeof(buffer)); 243 | } else { 244 | LOG(DEBUG) << "Skipped an IPv6 address for: " << i -> ifa_name; 245 | } 246 | break; 247 | } 248 | default: 249 | continue; 250 | } 251 | if (++ips > 1) ss << " "; 252 | ss << buffer; 253 | } 254 | freeifaddrs(addrs); 255 | if (ips == 0) return "null"; 256 | return DBObject::quote(ss.str()); 257 | } 258 | 259 | 260 | /** 261 | * Returns the name of the host. 262 | */ 263 | const string unix::host_name() { 264 | char buffer[1024]; 265 | if (gethostname(buffer, sizeof(buffer))) { 266 | perror("advanced shell history: Unix: gethostname"); 267 | } 268 | return DBObject::quote(buffer); 269 | } 270 | 271 | 272 | /** 273 | * Return the login name of the user entering commands. 274 | */ 275 | const string unix::login_name() { 276 | return DBObject::quote(getlogin()); 277 | } 278 | 279 | 280 | /** 281 | * Returns the abbreviated controlling TTY; the leading /dev/ is stripped. 282 | */ 283 | const string unix::tty() { 284 | string tty = DBObject::quote(ttyname(0)); 285 | if (tty.find("/dev/") == 1) { 286 | return "'" + tty.substr(6); 287 | } 288 | return tty; 289 | } 290 | 291 | 292 | /** 293 | * Returns the shell environment value for the argument variable. 294 | */ 295 | const string unix::env(const char * name) { 296 | return DBObject::quote(getenv(name)); 297 | } 298 | 299 | 300 | /** 301 | * Returns an integer-representation of a shell environment value. 302 | */ 303 | const string unix::env_int(const char * name) { 304 | return Util::to_string(atoi(getenv(name))); 305 | } 306 | -------------------------------------------------------------------------------- /src/unix.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __ASH_UNIX__ 18 | #define __ASH_UNIX__ 19 | 20 | #include 21 | 22 | using std::string; 23 | 24 | namespace ash { 25 | namespace unix { 26 | 27 | /** 28 | * This namespace provides a variety of UNIX-related functions. 29 | * 30 | * All functions return a quoted-string value, an int string value or a string 31 | * representation of the keyword "null". All are intended to be inserted 32 | * directly into SQL queries. 33 | */ 34 | const string cwd(); 35 | const string env(const char * name); 36 | const string env_int(const char * name); 37 | const string euid(); 38 | const string host_ip(); 39 | const string host_name(); 40 | const string login_name(); 41 | const string pid(); 42 | const string ppid(); 43 | const string shell(); 44 | const string time(); 45 | const string time_zone(); 46 | const string tty(); 47 | const string uid(); 48 | 49 | } // namespace unix 50 | } // namespace ash 51 | 52 | #endif /* __ASH_UNIX__ */ 53 | -------------------------------------------------------------------------------- /src/util.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "util.hpp" 18 | 19 | #include 20 | #include 21 | 22 | 23 | using namespace ash; 24 | using namespace std; 25 | 26 | 27 | /** 28 | * Converts an int to a string. 29 | */ 30 | string Util::to_string(int value) { 31 | static stringstream ss; 32 | ss.str(""); 33 | ss << value; 34 | return ss.str(); 35 | } 36 | -------------------------------------------------------------------------------- /src/util.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011 Carl Anderson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #ifndef __ASH_UTIL__ 18 | #define __ASH_UTIL__ 19 | 20 | 21 | #include 22 | 23 | using std::string; 24 | 25 | namespace ash { 26 | 27 | 28 | /** 29 | * This class is intended to hold all the commonly-used helper methods. 30 | */ 31 | class Util { 32 | public: 33 | static string to_string(int); 34 | }; 35 | 36 | 37 | } // namespace ash 38 | 39 | #endif /* __ASH_UTIL__ */ 40 | --------------------------------------------------------------------------------