pax_global_header00006660000000000000000000000064151245661120014515gustar00rootroot0000000000000052 comment=8ae21805b81681abe8ed73bcf3acbb65915b1b4c logserver/000077500000000000000000000000001512456611200130715ustar00rootroot00000000000000logserver/CMakeLists.txt000066400000000000000000000057221512456611200156370ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.10) set(CMAKE_VERBOSE_MAKEFILE ON) project(logserver VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED True) add_compile_options(-Wall -Wextra -pedantic -Wunreachable-code -Wshadow) file(GLOB SOURCES "src/*.cc") find_package(Curses REQUIRED) find_package(ZLIB REQUIRED) if (ZLIB_FOUND) message(STATUS "Found zlib libraries at ${ZLIB_LIBRARIES}") message(STATUS "Found zlib include dirs at ${ZLIB_INCLUDE_DIRS}") else() if (${MACOSX}) message(FATAL_ERROR "Sawdust dependency - Zlib not found. Install with `brew install zlib`") else () message(FATAL_ERROR "Sawdust dependency - Zlib not found. Install with `sudo apt-get install zlib1g-dev`") endif () endif () add_executable(logserver ${SOURCES}) add_executable(fuzzserver ${SOURCES}) target_compile_definitions(fuzzserver PRIVATE __FUZZ_IX__) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -march=native -flto") if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) else() add_compile_options(-O0 -g) endif() target_include_directories(logserver PRIVATE "${PROJECT_SOURCE_DIR}" "${CURSES_INCLUDE_DIR}" ) target_link_libraries(logserver PRIVATE "${CURSES_LIBRARIES}" ZLIB::ZLIB) target_include_directories(fuzzserver PRIVATE "${PROJECT_SOURCE_DIR}" "${CURSES_INCLUDE_DIR}" ) target_link_libraries(fuzzserver PRIVATE "${CURSES_LIBRARIES}" ZLIB::ZLIB) install(TARGETS logserver RUNTIME DESTINATION bin) install(FILES README.md LICENSE DESTINATION share/doc/logserver) add_custom_command( OUTPUT "${CMAKE_SOURCE_DIR}/man/logserver.1.gz" COMMAND txt2man -s 1 -t logserver "${CMAKE_SOURCE_DIR}/man/logserver.txt" | gzip > "${CMAKE_SOURCE_DIR}/man/logserver.1.gz" DEPENDS man/logserver.txt VERBATIM ) add_custom_target( generate_man ALL DEPENDS man/logserver.1.gz ) install(FILES man/logserver.1.gz DESTINATION share/man/man1) file(GLOB TEST_SOURCES "tests/test_*.cc") add_executable(runtests ${TEST_SOURCES}) find_package(Catch2 3 REQUIRED) include(Catch) catch_discover_tests(runtests) target_link_libraries(runtests PRIVATE Catch2::Catch2WithMain ZLIB::ZLIB ) target_include_directories(runtests PRIVATE ${CMAKE_SOURCE_DIR}/src ) set(CPACK_GENERATOR "DEB") # Package metadata set(CPACK_PACKAGE_NAME "logserver") set(CPACK_PACKAGE_VERSION "1.13.2") set(CPACK_PACKAGE_CONTACT "logserver@potatocrunchcereal.com") set(CPACK_PACKAGE_DESCRIPTION "pager designed for rapid navigation with multiple keyword searching on giant log files") set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Joel Reardon") set(CPACK_DEBIAN_PACKAGE_SECTION "utils") set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") # Where to install (optional, defaults to /usr/local) set(CPACK_PACKAGE_INSTALL_DIRECTORY "/usr") # Add license info, if you want set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") include(CPack) logserver/LICENSE000066400000000000000000000764071512456611200141140ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. “This License” refers to version 3 of the GNU General Public License. “Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. “The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. A “covered work” means either the unmodified Program or a work based on the Program. To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. “Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. “Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. logserver/README.md000066400000000000000000000063521512456611200143560ustar00rootroot00000000000000# logserver pager designed for rapid navigation with multiple keyword searching on giant log files ## requirements - libz-dev - libncurses-dev - catch2 (for unit tests) That's it. ## building project uses cmake: - create a build subdirectory - cmake .. - make - optionally sudo make install or copy logserver to your $PATH ## packaging - cmake -B build -S . -DCMAKE_BUILD_TYPE=Release - cmake --build build - cpack --config build/CPackConfig.cmake ## using - command-stdout | logserver - logserver filename ## keybindings h for help! q: quit h: help page (this) n: toggle line numbers c: toggle colours !: insert line marker s: save current line to /tmp/logserver_save S: save current view to /tmp/logserver_save b: break a long line into reasonable length new lines B: breaks the line using the next keypress as breakpoint (e.g., comma, semicolon, etc.) i: give more info for a line (repeat to try harder) I: give more info for a bunch of lines around (repeat to try harder and farther) t: toggle tab inference dd: delete line |: begin a pipe command :: go to a line number t: turn on tab inferrance and tabbbing y: copy current line to system clipboard #: write a comment, gets saved in a file called storytime.txt with nearby context lines e: edit the current line (useful to tidy for a screenshot) S: save the file (takes a filename) s: save the current line (takes a filename) C: wipes entire contents but keeps streaming in new data (navigation) arrows: move in that direction G or HOME: move to top line T or END: move to bottom line PAGE-UP: move up some lines (shift for many lines) PAGE-DOWN: move down some lines (shift for many lines) shift-HOME: move to start of current line shift-END: move to end of current line shift-LEFT: move to previous matching keyword on line shift-RIGHT: move to next matching keyword on line shift-DOWN: move to next matching keyword downwards shift-UP: move to next matching keyword upwards J: set a jump point at line j: go to next jump point (search) /: start a search ^: start a left anchored search $: start a right anchored search \: start a remove search +: add lines of context around matches -: remove lines of context around matches p: pin the current line so it appears in searches ENTER: finish search term BACKSPACE: remove the last search term TAB: switch between ALL (off), AND and OR mode for search terms (stacks) %: filter current matching lines into a new page, if in ALL mode then takes content between nearest line markers in each direction (i.e., from !) ESC: pop current page, go back to old one ENTER: finish a pipe and run command to new view <: moves left on view stack >: moves right on view stack logserver/man/000077500000000000000000000000001512456611200136445ustar00rootroot00000000000000logserver/man/logserver.txt000066400000000000000000000165511512456611200164250ustar00rootroot00000000000000NAME logserver - a fast pager designed for rapid navigation and multiple keyword searching on giant log files SYNOPSIS logserver [inputfile] DESCRIPTION logserver is an ncurses pager that streams a file and makes searching and navigating it easy. It is designed to help explore and search log files when you may not exactly know what you are looking for. It begins in command mode and operates with keybindings. Some keybindings switches to input mode, where the keys are accepted as an input until enter (accept) or escape (reject) is used to return to command mode. The centre line is highlighted and corresponds to the current line that is being viewed. Commands that operate on a line thereby operate on this highlighted one. KEYBINDINGS FOR NAVIGATION: arrows: Move around in the file. Long lines do not wrap so left and right moves accordingly. home: Move to the top of the file. end: Move to one-past the end of the file, which will display streaming data shift+home: move left to the start of the line shift+end: move right to the end of the line page-up: move to the top line visible page-down: move to the bottom line visible colon: accepts a number afterwards, and moves to that line number KEYBINDINGS FOR SEARCHING: slash: Accepts a keyword afterwards, and adds it as a search term. backslash: Accepts a keyword afterwards, and adds it as a reverse search. caret: Accepts a keyword afterwards, and adds it as a starts with search. dollar sign: Accepts a keyword afterwards, and adds it as an ends with search. tab: Alternates among search modes. ALL mode shows all lines and highlights matching keywords. OR mode (disjunctive) shows lines that match any keyword. AND mode (conjunctive) shows lines that match all keywords. shift+left: move left on the current line to the next matching keyword shift+right: move right on the current line to the next matching keyword shift+up: In ALL mode, moves up to the next line that matches any keyword. In OR mode, moves up to the next line that matches a keyword that is not matched on the current line. Useful for staying in OR mode but skipping large amounts of the same match. shift+down: Same as shift+up but searches downwards. backspace: Removes the most-recently added search term. plus: Add one more line of context around matching lines. minus: Remove one line of context around matching lines. KEYBINDINGS FOR LINE OPERATIONS: octothorpe: Accepts a string afterwards, and adds the current line and surrounding view along with that comment to a file in current directory called storytime.txt. letter e: Accepts a string starting with the current line. Changes to that line are reflected in the display (but not the original file). letter b: Breaks a long line up and inserts the new lines. This uses a number of heuristics in an attempt to be elegant. It uses spaces and punctuation to give a ragged-right in text. It uses ampersands and equals to infer HTTP query strings and breaks on the ampersand. It uses quotes and braces to infer JSON for pretty printing, and it uses periodic backslash-n to infer escape newlines and breaks on those. letter B: Accepts a single character next, and performs the break functionality described for the letter b using that specific character, i.e., replaces that character with newlines. letter i: Intelligence for lines. Replaces UNIX timestamps with human time, and looks for sequences of base64 or base16 encoded text based on printable characters after decoding. Repeated pressing of 'i' softens the heuristics of how much text needs to be printable. letter d: If hit twice in a row, deletes the current line from the display (not from the original file). asterisk: Pins the current line. When searching for keywords pinned lines will appear in OR and AND mode despite not matching. letter s: Accepts a string afterwards, and writes the current line to the file specified by that string. letter f: Follows a link on the current line. If logserver is given the output of grep -rn, then each line will link to that file and line number and 'f' will follow it. If logserver is reading a ctags file, then each line will be a link to that target. letter m: Merge the next line to the end of the current line. KEYBINDINGS FOR GLOBAL OPERATIONS AND STACKED VIEWS letter q: Quits logserver. letter n: Toggle line numbers on and off. letter c: Toggle colouring on and off. letter S: Accepts a string afterwards, and writes the entire log file to the file specified by that string. This is useful when data is bring streamed into logserver by program output. letter h: Launch the help screen, pushes on the stack. letter C: Clears the entire contents of the log. This is useful when streaming in data, e.g., a device log, and you want the logs relevant to a particular event that is about to be triggered. exclamation: inserts a new pinned dash line at the current position. Appends it if the current line is one-past the end. Useful for separating segments of the log, such as the debug log corresponding to right before pressing a button and right after the program crashes, so just the area between those events is conspicuous. percent: In OR and AND mode, applies the current filter and creates a new view with just the matching lines and puts that on the stack (i.e., some percent of the logs). In ALL mode, goes up and down from the current position searching for a pinned like, such as one created by an exclamation, and pushes a new view on the stack bounded by those (or the top and bottom if none are found). less-than: Moves left on the stack of views. greater-than: Moves right on the stack of views. escape: Pops the top most (right most) view on the stack. Does nothing if there is only one view. pipe: Accepts a string afterwards and runs that command, passing the current view as stdin, and pushing a new view on the stack with the stdout of the command as its contents. For security purposes the set of commands that can be run is limited to the following: cat, sort, uniq, ls, grep, cut, tr, sed, awk, fgrep, which, whoami, base64, echo, file, wc, xsel, mplayer. letter T: Consider the next key pressed as the tab character, so T-comma can be for CSVs letter t: Toggle tab mode, where tab character and column widths are used to align tabular data numbers 0-9: In tab mode, toggle suppression of column number. Columns are indexed starting at 1, with number ten as 0 EXAMPLES Try this command to view the system log in logserver: $ journalctl | logserver $ logserver /var/log/apache2/access.log SEE ALSO less(1), more(1) COPYRIGHT Copyright (C) 2017-2025 Joel Reardon This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. See the file LICENSE. If not, see . AUTHOR Joel Reardon logserver/src/000077500000000000000000000000001512456611200136605ustar00rootroot00000000000000logserver/src/base64.h000066400000000000000000000110361512456611200151160ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __BASE64__H__ #define __BASE64__H__ #include #include #include #include #include using namespace std; /* helper class for encoding and decoding base64 strings */ class Base64 { public: static size_t encoded_len(size_t len) { return 1 + ((len + 2) / 3 * 4); } static string encode(const char* str, size_t len) { stringstream ss; static const string base = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"; size_t i; for (i = 0; i + 2 < len; i += 3) { ss << base[(str[i] >> 2) & 0x3f]; ss << base[((str[i] & 0x3) << 4) | ((int) (str[i + 1] & 0xf0) >> 4)]; ss << base[((str[i + 1] & 0xf) << 2) | ((int) (str[i + 2] & 0xc0) >> 6)]; ss << base[str[i + 2] & 0x3f]; } if (i < len) { ss << base[(str[i] >> 2) & 0x3f]; if (i == (len - 1)) { ss << base[((str[i] & 0x3) << 4)]; ss << '='; } else { ss << base[((str[i] & 0x3) << 4) | ((int) (str[i + 1] & 0xf0) >> 4)]; ss << base[((str[i + 1] & 0xF) << 2)]; } ss << '='; } return ss.str(); } static string encode(const string_view& str) { return encode(str.data(), str.length()); } static string reduce(const string_view& s) { stringstream ss; for (const auto& x : s) { if (x >= 'a' && x <= 'z') ss << x; if (x >= 'A' && x <= 'Z') ss << x; if (x >= '0' && x <= '9') ss << x; if (x == '+' || x == '/' || x == '=') ss << x; } return ss.str(); } static string decode(const string& s) { if (s.empty()) return ""; static const int _index [256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 63, 62, 62, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 63, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 }; return decode(s, _index); } static string decode(const string_view& s, const string& alphabet) { // "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" assert(alphabet.length() == 64); int index[256]; map where; for (size_t i = 0; i < 64; ++i) { assert(!where.count(alphabet[i])); where[alphabet[i]] = i; } for (size_t i = 0; i < 256; ++i) { index[i] = 0; if (where.count((char) i)) { index[i] = where[i]; } } return decode(s, index); } static string decode(const string_view& s, const int _index[256]) { const uint8_t* p = reinterpret_cast(s.data()); size_t len = s.length(); size_t pad = len > 0 && (len % 4 || p[len - 1] == '='); const size_t l = ((len + 3) / 4 - pad) * 4; string str(l / 4 * 3 + pad, '\0'); for (size_t i = 0, j = 0; i < l; i += 4) { int n = (_index[p[i]] << 18) | (_index[p[i + 1]] << 12) | (_index[p[i + 2]] << 6) | _index[p[i + 3]]; if (p[i] == '\n' || p[i + 1] == '\n' || p[i + 2] == '\n' || p[i + 3] == '\n') return decode(reduce(s)); str[j++] = n >> 16; str[j++] = (n >> 8) & 0xFF; str[j++] = n & 0xFF; } if (pad) { int n = (_index[p[l]] << 18) | (_index[p[l + 1]] << 12); str[str.size() - 1] = (n >> 16); if (len > l + 2 && p[l + 2] != '=') { n |= (_index[p[l + 2]] << 6); str.push_back((n >> 8) & 0xFF); } } return str; } }; #endif // __BASE64__H__ logserver/src/base_line_provider.h000066400000000000000000000160731512456611200176730ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __BASE_LINE_PROVIDER__H__ #define __BASE_LINE_PROVIDER__H__ #include #include #include #include #include #include #include "colour.h" #include "constants.h" #include "exit_signal.h" #include "i_line_provider.h" #include "i_log_lines.h" #include "line.h" #include "utf8.h" #include using namespace std; /* BaseLineProvider provides common functionality for all line providers sub * classes but is not a full implementation itself */ class BaseLineProvider : public ILineProvider { public: /* Takes a LogLines interface which just allows adding new lines and * querying/setting end-of-file */ explicit BaseLineProvider(ILogLines* ll) : _ll(ll), _exit(false) {} /* starts thread that runs the implementing provider */ virtual inline void start() override { _runner.reset(new thread(&BaseLineProvider::loader, this)); } /* must be overriden */ virtual string get_line([[maybe_unused]] size_t pos) override { assert(0); return ""; } /* tells thread to stop and waits */ virtual ~BaseLineProvider() { _exit = true; if (_runner.get()) _runner->join(); for (const auto& x : _lines) delete x; } static pair get_string_and_format(const string_view& in) { pair ret; bool underline = false; bool bold = false; int cur_colour = 0; optional colour; for (size_t i = 0; i < in.length(); ++i) { char c = in[i]; if (i && c == '\b' && i + 1 < in.length()) [[unlikely]] { char left = in[i - 1]; char right = in[i + 1]; if (left == right) { ret.second[ret.second.length() - 1] = '\32'; } else { // use right if we stored an underline if (left == '_') { ret.first[ret.first.length() - 1] = right; } ret.second[ret.second.length() - 1] = '\64'; } ++i; } else if (c == 0x1b) [[unlikely]] { colour = nullopt; i = parse_ansi(in, i, &bold, &underline, &colour); if (colour) cur_colour = *colour; continue; } else if (!isspace(c) && isprint(c) && (cur_colour || bold || underline)) [[unlikely]] { char to_add = cur_colour; if (underline) to_add |= G::UNDERLINE; else if (bold) to_add |= G::BOLD; ret.first += c; ret.second += to_add; } else { optional decode = UTF8::simplify(in, &i); if (decode) [[unlikely]] { ret.first += *decode; ret.second += string(decode->size(), '\0'); } else [[likely]] { ret.first += c; ret.second += '\0'; } } } return ret; } protected: /* makes a Line type from a string, considering possible terminal * escapes and printing */ static Line* make_line(const string& line) { if (line.find_first_of("\b\x1b") == string::npos) return new Line(line); // we have either backspace or ansi, or both. pair stringformat = get_string_and_format(line); return new Line(stringformat.first, stringformat.second); } /* returns a first number position in in, or nullopt if none present */ static optional move_to_number(const string_view& in) { size_t i = 0; while (i < in.size()) { if (in[i] >= '0' && in[i] <= '9') return string(in.substr(i)); ++i; } return nullopt; } /* if data[pos] is escape and what follows is valid ansi code, then set * the boolean bold and underline appropriately and return index of * string at the end of the sequence. return input position if it cannot * be parsed or it is not ending as an 'm' ansi code */ static size_t parse_ansi(const string_view& data, size_t pos, bool* bold, bool* underline, optional* colour) { assert(data[pos] == 0x1b); if (pos + 2 >= data.size()) return pos; if (data[pos + 1] != '[') return pos; size_t end = data.find('m', pos); if (end == string::npos) return pos; pos += 2; // check valid ansi sequence until m for (size_t i = pos; i < end; ++i) { if (data[i] != ';' && data[i] < '0' && data[i] > '9') return pos; } optional format = move_to_number( data.substr(pos, end - pos)); size_t subpos; while (format) { try { int val = stoi(*format, &subpos); if (val == 4) { *underline = true; } else if (val == 1) { *bold = true; } else if (val == 24) { *underline = false; } else if (val == 22) { *bold = false; } else if (val == 0) { *underline = false; *bold = false; } else { *colour = Colour::ansi_colour(val); } format = move_to_number(format->substr(subpos)); } catch (...) { *underline = false; *bold = false; *colour = 0; break; } } return end; } /* adds a new string to the LogLines. returns false if user has signalled * to quit so the thread stop */ virtual inline bool add_line(const string& line) { return add_line(make_line(line)); } /* queue a string to add as a line to avoid locking loglines */ virtual inline bool queue_line(const string& line) { return queue_line(make_line(line)); } /* queue a line to add to avoid locking loglines */ virtual inline bool queue_line(Line* line) { if (exit()) [[unlikely]] return false; _lines.emplace_back(line); if (_lines.size() > 4096) flush_lines(); return true; } /* flush the queued lines to loglines */ virtual inline void flush_lines() { if (_lines.empty()) [[unlikely]] return; _ll->add_lines(&_lines); assert(_lines.empty()); } /* takes ownership and adds a new Line string to the LogLines. returns * false if the user has signalled to quit */ virtual inline bool add_line(Line* line) { if (exit()) [[unlikely]] { delete line; return false; } _ll->add_line(line); return true; } /* signals we have no more lines to come */ virtual inline void eof() { _ll->set_eof(true); } /* signals we may be wrong about no more lines and more are coming, * e.g, if we are serving a file, and that file has been appended */ virtual inline void unset_eof() { _ll->set_eof(false); } /* returns true if we are shutting down. */ virtual inline bool exit() const { if (_exit) return true; return ExitSignal::check(false); } /* default does nothing */ virtual void loader() { } /* LogLines we push our lines to */ ILogLines* _ll; /* thread that finds lines for LogLines to serve */ unique_ptr _runner; /* true if we are shutting down */ bool _exit; /* list of queued lines to add */ list _lines; }; #endif // __BASE_LINE_PROVIDER__H__ logserver/src/colour.h000066400000000000000000000070701512456611200153400ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __COLOUR__H__ #define __COLOUR__H__ #include #include #include #include "constants.h" using namespace std; /* class that performs colour-related helper functions */ class Colour { public: // maps keyword index to a colour initialized in ncurses static int keyword_number_to_colour(int number) { static const vector defaults = { COLOR_RED, COLOR_BLUE, COLOR_GREEN, COLOR_MAGENTA, COLOR_CYAN, COLOR_YELLOW }; static const vector many_defaults = { COLOR_RED, 8 + COLOR_BLUE, COLOR_GREEN, 8 + COLOR_MAGENTA, 8 + COLOR_CYAN, COLOR_YELLOW, 8 + COLOR_RED, COLOR_BLUE, 8 + COLOR_GREEN, COLOR_MAGENTA, COLOR_CYAN, 8 + COLOR_YELLOW }; if (has_brights()) return many_defaults[number % many_defaults.size()]; return defaults[number % defaults.size()]; } // returns true if console supports brights without bolding static bool has_brights() { #ifdef CATCH_CONFIG_MAIN return false; #else return COLORS >= 16; #endif } /* 0 is used as default of white on black. converts ansi colour code to * the ncurses colour index */ static optional ansi_colour(int val) { switch (val) { case 30: return COLOR_WHITE; case 31: return COLOR_RED; case 32: return COLOR_GREEN; case 33: return COLOR_YELLOW; case 34: return COLOR_BLUE; case 35: return COLOR_MAGENTA; case 36: return COLOR_CYAN; case 37: return COLOR_BLACK; } if (!has_brights()) return nullopt; switch (val) { case 90: return 8 + COLOR_WHITE; case 91: return 8 + COLOR_RED; case 92: return 8 + COLOR_GREEN; case 93: return 8 + COLOR_YELLOW; case 94: return 8 + COLOR_BLUE; case 95: return 8 + COLOR_MAGENTA; case 96: return 8 + COLOR_CYAN; case 97: return 8 + COLOR_BLACK; } return nullopt; } /* initializes the ncurses colour index */ static void init_curses_colours() { assert(has_colors()); start_color(); init_pair(0, COLOR_WHITE, COLOR_BLACK); init_pair(1, COLOR_RED, COLOR_BLACK); init_pair(2, COLOR_GREEN, COLOR_BLACK); init_pair(3, COLOR_YELLOW, COLOR_BLACK); init_pair(4, COLOR_BLUE, COLOR_BLACK); init_pair(5, COLOR_MAGENTA, COLOR_BLACK); init_pair(6, COLOR_CYAN, COLOR_BLACK); init_pair(7, COLOR_BLACK, COLOR_WHITE); if (!has_brights()) return; init_pair(8, 8 + COLOR_WHITE, COLOR_BLACK); init_pair(9, 8 + COLOR_RED, COLOR_BLACK); init_pair(10, 8 + COLOR_GREEN, COLOR_BLACK); init_pair(11, 8 + COLOR_YELLOW, COLOR_BLACK); init_pair(12, 8 + COLOR_BLUE, COLOR_BLACK); init_pair(13, 8 + COLOR_MAGENTA, COLOR_BLACK); init_pair(14, 8 + COLOR_CYAN, COLOR_BLACK); init_pair(15, 8 + COLOR_BLACK, COLOR_WHITE); } }; #endif // __COLOUR__H__ logserver/src/config.h000066400000000000000000000046511512456611200153040ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __IB__CONFIG__H__ #define __IB__CONFIG__H__ #include #include #include #include using namespace std; /* Config class storing some flags and string. Singleton pattern where _() * provides caller with a shared global config. It is thread safe. */ class Config { public: Config() {} virtual ~Config() {} // provide singleton instance of config static Config* _() { static Config _config; return &_config; } // returns the value for a key virtual string get(const string& key) { unique_lock ul(_m); return _name_to_string[key]; } // sets a key-value pair virtual void set(const string& key, const string& val) { unique_lock ul(_m); _name_to_string[key] = val; } // enables a boolean flag virtual void flag_on(const string& key) { unique_lock ul(_m); _name_to_string[key] = "true"; } // disables a boolean flag virtual void flag_off(const string& key) { unique_lock ul(_m); _name_to_string[key] = "false"; } // toggles a boolean flag virtual void flag_toggle(const string& key) { unique_lock ul(_m); if (!_name_to_string.count(key)) _name_to_string[key] = "false"; _name_to_string[key] = flag_locked(key) ? "false" : "true"; } // returns the value for a boolean flag virtual bool flag(const string& key) { unique_lock ul(_m); return flag_locked(key); } protected: virtual bool flag_locked(const string& key) { if (!_name_to_string.count(key)) return false; if (_name_to_string[key] == "true") return true; return false; } // disable copy Config(const Config&); // map of key to values for config map _name_to_string; // for threadsafety mutex _m; }; #endif // __IB__CONFIG__H__ logserver/src/constants.h000066400000000000000000000066311512456611200160530ustar00rootroot00000000000000#ifndef __CONSTANTS__H__ #define __CONSTANTS__H__ #include #include #include #include #include #include #include using namespace std; // TODO: move other relevant constants and configurable values here /* Global static constants holder and utility functions */ class G { public: /* timer to check memory */ static constexpr int USAGE_TIMER = 20; /* format string colours */ static constexpr int COLON_COLOUR = 6; static constexpr int CURSOR_COLOUR = 7; /* used to signal an unset position */ static constexpr size_t NO_POS = static_cast(-1); /* used in searching to signal search has reached end of the file */ static constexpr size_t EOF_POS = static_cast(-1000); // signal format for bold static constexpr size_t LOWEST_MODIFIER = 32; static constexpr size_t BOLD = 32; // signal format for underline static constexpr size_t UNDERLINE = 64; // used to signal a weak colour that will be overrided with another static constexpr size_t WEAK = 128; /* constants for directions */ static constexpr size_t DIR_DOWN = 1; static constexpr size_t DIR_UP = 0; // TODO perhaps use the current width of terminal instead static constexpr size_t LINE_WIDTH = 80; // how many lines get searched at a time before release the log lines // lock static constexpr size_t SEARCH_RANGE = 1024; /* returns half the width of the screen, to implement left and right * moving */ static size_t h_shift() { return h_shift(nullopt); } /* renderer will update this value to half the number of cols to support * left right moving. pass nullopt to read the stored value. * start with a default of 40 assuming 80 col term. */ static size_t h_shift(optional value) { static size_t _val = 40; if (value) _val = *value; return _val; } // dash line for separating static constexpr const char* DASH = "---------------------------------------------"; /* constants for matching mode */ static constexpr int FILTER_NONE = 0; static constexpr int FILTER_AND = 1; static constexpr int FILTER_OR = 2; // number of filter types static constexpr int FILTER_TOTAL = 3; // return a prefix and errno with string of error static string errno_string(const string& prefix, int error) { stringstream ss; ss << prefix << " errno=" << error << " " << strerror(error); return ss.str(); } // return lowercase version of string static string to_lower(const string_view& data) { string ret; ret.reserve(data.length()); for (const char& c : data) { ret += tolower(c); } return ret; } // clears the debug log on starting static void clear_log() { if (filesystem::exists("logserver.debug")) { ofstream fout("logserver.debug", ios::out); } } // writes a log message to debug log static void log(const string& s) { ofstream fout("logserver.debug", ios::app); fout << s << endl; } // turns a set to a vector template static vector set_to_vec(const set& in) { vector out; for (const auto& x : in) out.push_back(x); return out; } // gets full path for a possibly relative one static string realpath(const string_view& path) { string ret = ""; char* delete_me = ::realpath(path.data(), nullptr); if (delete_me) ret = string(delete_me); free(delete_me); return ret; } }; #endif // __CONSTANTS__H__ logserver/src/contrib/000077500000000000000000000000001512456611200153205ustar00rootroot00000000000000logserver/src/contrib/izstream.hpp000066400000000000000000000213221512456611200176670ustar00rootroot00000000000000/* zstream-cpp Library License: -------------------------- The zlib/libpng License Copyright (c) 2003 Jonathan de Halleux. This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution Author: Jonathan de Halleux, dehalleux@pelikhan.com, 2003 Gero Mueller, post@geromueller.de, 2015 */ #ifndef INPUT_ZIP_STREAM_HPP #define INPUT_ZIP_STREAM_HPP #include #include #include #include #include "zstream_common.hpp" namespace zstream { /** \brief A stream decorator that takes compressed input and unzips it to a istream. The class wraps up the deflate method of the zlib library 1.1.4 http://www.gzip.org/zlib/ */ template, typename ElemA = std::allocator, typename ByteT = unsigned char, typename ByteAT = std::allocator > class basic_unzip_streambuf: public std::basic_streambuf { public: typedef std::basic_istream& istream_reference; typedef ElemA char_allocator_type; typedef ByteT byte_type; typedef ByteAT byte_allocator_type; typedef byte_type* byte_buffer_type; typedef typename std::basic_streambuf::char_type char_type; typedef typename std::basic_streambuf::int_type int_type; typedef std::vector byte_vector_type; typedef std::vector char_vector_type; /** Construct a unzip stream * More info on the following parameters can be found in the zlib documentation. */ basic_unzip_streambuf(istream_reference istream_, size_t window_size_, size_t read_buffer_size_, size_t input_buffer_size_); ~basic_unzip_streambuf(); int_type underflow(); /// returns the compressed input istream istream_reference get_istream() { return m_istream; } /// returns the zlib stream structure z_stream& get_zip_stream() { return m_zip_stream; } /// returns the latest zlib error state int get_zerr() const { return m_err; } /// returns the crc of the uncompressed data so far unsigned int get_crc() const { return m_crc; } /// returns the number of uncompressed bytes unsigned int get_out_size() const { return m_zip_stream.total_out; } /// returns the number of read compressed bytes unsigned long get_in_size() const { return m_zip_stream.total_in; } private: void put_back_from_zip_stream(); std::streamsize unzip_from_stream(char_type*, std::streamsize); size_t fill_input_buffer(); istream_reference m_istream; z_stream m_zip_stream; int m_err; byte_vector_type m_input_buffer; char_vector_type m_buffer; long m_crc; }; /*! \brief Base class for unzip istreams Contains a basic_unzip_streambuf. */ template, typename ElemA = std::allocator, typename ByteT = unsigned char, typename ByteAT = std::allocator > class basic_zip_istreambase: virtual public std::basic_ios { public: typedef std::basic_istream& istream_reference; typedef basic_unzip_streambuf unzip_streambuf_type; basic_zip_istreambase(istream_reference ostream_, size_t window_size_, size_t read_buffer_size_, size_t input_buffer_size_) : m_buf(ostream_, window_size_, read_buffer_size_, input_buffer_size_) { this->init(&m_buf); } /// returns the underlying unzip istream object unzip_streambuf_type* rdbuf() { return &m_buf; } /// returns the zlib error state int get_zerr() const { return m_buf.get_zerr(); } /// returns the uncompressed data crc long get_crc() const { return m_buf.get_crc(); } /// returns the uncompressed data size long get_out_size() const { return m_buf.get_out_size(); } /// returns the compressed data size unsigned long get_in_size() const { return m_buf.get_in_size(); } private: unzip_streambuf_type m_buf; }; /*! \brief A zipper istream This class is a istream decorator that behaves 'almost' like any other ostream. At construction, it takes any istream that shall be used to input of the compressed data. Simlpe example: \code // create a stream on zip string istringstream istringstream_( ostringstream_.str()); // create unzipper istream zip_istream unzipper( istringstream_); // read and unzip unzipper>>f_r>>d_r>>ui_r>>ul_r>>us_r>>c_r>>dum_r; \endcode */ template, typename ElemA = std::allocator, typename ByteT = unsigned char, typename ByteAT = std::allocator > class basic_gzip_istream: public basic_zip_istreambase, public std::basic_istream { public: typedef typename std::basic_istream::char_type char_type; typedef std::basic_istream& istream_reference; typedef basic_zip_istreambase zip_istreambase_type; typedef std::basic_istream istream_type; typedef unsigned char byte_type; /** Construct a unzipper stream * * \param istream_ input buffer * \param window_size_ * \param read_buffer_size_ * \param input_buffer_size_ */ basic_gzip_istream(istream_reference istream_, size_t window_size_ = 15, size_t read_buffer_size_ = detail::default_buffer_size, size_t input_buffer_size_ = detail::default_buffer_size) : zip_istreambase_type(istream_, window_size_, read_buffer_size_, input_buffer_size_), istream_type(this->rdbuf()), m_gzip_crc( 0), m_gzip_data_size(0) { if (this->rdbuf()->get_zerr() == Z_OK) check_header(); } /// reads the gzip header void read_footer(); /** return crc check result When you have finished reading the compressed data, call read_footer to read the uncompressed data crc. This method compares it to the crc of the uncompressed data. \return true if crc check is succesful */ bool check_crc() const { return this->get_crc() == m_gzip_crc; } /// return data size check bool check_data_size() const { return this->get_out_size() == m_gzip_data_size; } /// return the crc value in the file unsigned int get_gzip_crc() const { return m_gzip_crc; } /// return the data size in the file unsigned int get_gzip_data_size() const { return m_gzip_data_size; } protected: static void read_long(istream_reference in_, unsigned int& x_); int check_header(); unsigned int m_gzip_crc; unsigned int m_gzip_data_size; }; template, typename ElemA = std::allocator, typename ByteT = unsigned char, typename ByteAT = std::allocator > class basic_zip_istream: public basic_zip_istreambase, public std::basic_istream { public: typedef typename std::basic_istream::char_type char_type; typedef std::basic_istream& istream_reference; typedef basic_zip_istreambase zip_istreambase_type; typedef std::basic_istream istream_type; typedef unsigned char byte_type; /** Construct a unzipper stream * * \param istream_ input buffer * \param window_size_ * \param read_buffer_size_ * \param input_buffer_size_ */ basic_zip_istream(istream_reference istream_, size_t window_size_ = 15, size_t read_buffer_size_ = detail::default_buffer_size, size_t input_buffer_size_ = detail::default_buffer_size) : zip_istreambase_type(istream_, window_size_, read_buffer_size_, input_buffer_size_), istream_type(this->rdbuf()) { } }; /// A typedef for basic_zip_istream typedef basic_gzip_istream igzstream; /// A typedef for basic_zip_istream typedef basic_gzip_istream wigzstream; /// A typedef for basic_zip_istream typedef basic_zip_istream izstream; /// A typedef for basic_zip_istream typedef basic_zip_istream wizstream; } // zstream #include "izstream_impl.hpp" #endif logserver/src/contrib/izstream_impl.hpp000066400000000000000000000171521512456611200207160ustar00rootroot00000000000000/* zstream-cpp Library License: -------------------------- The zlib/libpng License Copyright (c) 2003 Jonathan de Halleux. This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution Author: Jonathan de Halleux, dehalleux@pelikhan.com, 2003 Gero Mueller, post@geromueller.de, 2015 */ #ifndef INPUT_ZIP_STREAM_IMPL_HPP #define INPUT_ZIP_STREAM_IMPL_HPP #include "izstream.hpp" #include #include namespace zstream { template basic_unzip_streambuf::basic_unzip_streambuf( istream_reference istream_, size_t window_size_, size_t read_buffer_size_, size_t input_buffer_size_) : m_istream(istream_), m_input_buffer(input_buffer_size_), m_buffer( read_buffer_size_), m_crc(0) { // setting zalloc, zfree and opaque m_zip_stream.zalloc = (alloc_func) 0; m_zip_stream.zfree = (free_func) 0; m_zip_stream.next_in = NULL; m_zip_stream.avail_in = 0; m_zip_stream.avail_out = 0; m_zip_stream.next_out = NULL; m_err = inflateInit2(&m_zip_stream, -static_cast(window_size_)); this->setg(&(m_buffer[0]) + 4, // beginning of putback area &(m_buffer[0]) + 4, // read position &(m_buffer[0]) + 4); // end position } template size_t basic_unzip_streambuf::fill_input_buffer() { m_zip_stream.next_in = &(m_input_buffer[0]); m_istream.read((char_type*) (&(m_input_buffer[0])), static_cast(m_input_buffer.size() / sizeof(char_type))); return m_zip_stream.avail_in = m_istream.gcount() * sizeof(char_type); } template basic_unzip_streambuf::~basic_unzip_streambuf() { inflateEnd(&m_zip_stream); } template typename basic_unzip_streambuf::int_type basic_unzip_streambuf< Elem, Tr, ElemA, ByteT, ByteAT>::underflow() { if (this->gptr() && (this->gptr() < this->egptr())) return *reinterpret_cast(this->gptr()); int n_putback = static_cast(this->gptr() - this->eback()); if (n_putback > 4) n_putback = 4; memcpy(&(m_buffer[0]) + (4 - n_putback), this->gptr() - n_putback, n_putback * sizeof(char_type)); int num = unzip_from_stream(&(m_buffer[0]) + 4, static_cast((m_buffer.size() - 4) * sizeof(char_type))); if (num <= 0) // ERROR or EOF return EOF; // reset buffer pointers this->setg(&(m_buffer[0]) + (4 - n_putback), // beginning of putback area &(m_buffer[0]) + 4, // read position &(m_buffer[0]) + 4 + num); // end of buffer // return next character return *reinterpret_cast(this->gptr()); } template std::streamsize basic_unzip_streambuf::unzip_from_stream( char_type* buffer_, std::streamsize buffer_size_) { m_zip_stream.next_out = (byte_buffer_type) buffer_; m_zip_stream.avail_out = static_cast(buffer_size_ * sizeof(char_type)); size_t count = m_zip_stream.avail_in; do { if (m_zip_stream.avail_in == 0) count = fill_input_buffer(); if (m_zip_stream.avail_in) { m_err = inflate(&m_zip_stream, Z_SYNC_FLUSH); } } while (m_err == Z_OK && m_zip_stream.avail_out != 0 && count != 0); // updating crc m_crc = crc32(m_crc, (byte_buffer_type) buffer_, buffer_size_ - m_zip_stream.avail_out / sizeof(char_type)); std::streamsize n_read = buffer_size_ - m_zip_stream.avail_out / sizeof(char_type); // check if it is the end if (m_err == Z_STREAM_END) put_back_from_zip_stream(); return n_read; } template void basic_unzip_streambuf::put_back_from_zip_stream() { if (m_zip_stream.avail_in == 0) return; m_istream.clear(std::ios::goodbit); m_istream.seekg(-static_cast(m_zip_stream.avail_in), std::ios_base::cur); m_zip_stream.avail_in = 0; } template int basic_gzip_istream::check_header() { int method; /* method byte */ int flags; /* flags byte */ uInt len; int c; int err = 0; z_stream& zip_stream = this->rdbuf()->get_zip_stream(); /* Check the gzip magic header */ for (len = 0; len < 2; len++) { c = (int) this->rdbuf()->get_istream().get(); if (c != detail::gz_magic[len]) { if (len != 0) this->rdbuf()->get_istream().unget(); if (c != EOF) { this->rdbuf()->get_istream().unget(); } err = zip_stream.avail_in != 0 ? Z_OK : Z_STREAM_END; return err; } } method = (int) this->rdbuf()->get_istream().get(); flags = (int) this->rdbuf()->get_istream().get(); if (method != Z_DEFLATED || (flags & detail::gz_reserved) != 0) { err = Z_DATA_ERROR; return err; } /* Discard time, xflags and OS code: */ for (len = 0; len < 6; len++) this->rdbuf()->get_istream().get(); if ((flags & detail::gz_extra_field) != 0) { /* skip the extra field */ len = (uInt) this->rdbuf()->get_istream().get(); len += ((uInt) this->rdbuf()->get_istream().get()) << 8; /* len is garbage if EOF but the loop below will quit anyway */ while (len-- != 0 && this->rdbuf()->get_istream().get() != EOF) ; } if ((flags & detail::gz_orig_name) != 0) { /* skip the original file name */ while ((c = this->rdbuf()->get_istream().get()) != 0 && c != EOF) ; } if ((flags & detail::gz_comment) != 0) { /* skip the .gz file comment */ while ((c = this->rdbuf()->get_istream().get()) != 0 && c != EOF) ; } if ((flags & detail::gz_head_crc) != 0) { /* skip the header crc */ for (len = 0; len < 2; len++) this->rdbuf()->get_istream().get(); } err = this->rdbuf()->get_istream().eof() ? Z_DATA_ERROR : Z_OK; return err; } template void basic_gzip_istream::read_footer() { read_long(this->rdbuf()->get_istream(), m_gzip_crc); read_long(this->rdbuf()->get_istream(), m_gzip_data_size); } template void basic_gzip_istream::read_long( istream_reference in_, unsigned int& x_) { static const int size_ul = sizeof(unsigned int); static const int size_c = sizeof(char_type); static const int n_end = size_ul / size_c; in_.read(reinterpret_cast(&x_), n_end); } } // zstream #endif logserver/src/contrib/ozstream.hpp000066400000000000000000000227571512456611200177120ustar00rootroot00000000000000/* zstream-cpp Library License: -------------------------- The zlib/libpng License Copyright (c) 2003 Jonathan de Halleux. This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution Author: Jonathan de Halleux, dehalleux@pelikhan.com, 2003 Gero Mueller, post@geromueller.de, 2015 */ #ifndef OUTPUT_ZIP_STREAM_HPP #define OUTPUT_ZIP_STREAM_HPP #include #include #include #include #include #include "zstream_common.hpp" namespace zstream { /** \brief A stream decorator that takes raw input and zips it to a ostream. The class wraps up the inflate method of the zlib library */ template, typename ElemA = std::allocator, typename ByteT = unsigned char, typename ByteAT = std::allocator > class basic_zip_streambuf: public std::basic_streambuf { public: typedef std::basic_ostream& ostream_reference; typedef ElemA char_allocator_type; typedef ByteT byte_type; typedef ByteAT byte_allocator_type; typedef byte_type* byte_buffer_type; typedef typename std::basic_streambuf::char_type char_type; typedef typename std::basic_streambuf::int_type int_type; typedef std::vector byte_vector_type; typedef std::vector char_vector_type; /** Construct a zip stream * More info on the following parameters can be found in the zlib documentation. */ basic_zip_streambuf(ostream_reference ostream_, size_t level_, EStrategy strategy_, size_t window_size_, size_t memory_level_, size_t buffer_size_); ~basic_zip_streambuf(); int sync(); int_type overflow(int_type c); /** flushes the zip buffer and output buffer. This method should be called at the end of the compression. Calling flush multiple times, will lower the compression ratio. */ std::streamsize zfinish(); /// returns a reference to the output stream ostream_reference get_ostream() const { return m_ostream; } /// returns the latest zlib error status int get_zerr() const { return m_err; } /// returns the crc of the input data compressed so far. long get_crc() const { return m_crc; } /// returns the size (bytes) of the input data compressed so far. long get_in_size() const { return m_zip_stream.total_in; } /// returns the size (bytes) of the compressed data so far. long get_out_size() const { return m_zip_stream.total_out; } private: bool zip_to_stream(char_type*, std::streamsize); std::streamsize zip_write(int flag); ostream_reference m_ostream; z_stream m_zip_stream; int m_err; byte_vector_type m_output_buffer; char_vector_type m_buffer; long m_crc; }; /*! \brief Base class for zip ostreams Contains a basic_zip_streambuf. */ template, typename ElemA = std::allocator, typename ByteT = unsigned char, typename ByteAT = std::allocator > class basic_zip_ostreambase: virtual public std::basic_ios { public: typedef std::basic_ostream& ostream_reference; typedef basic_zip_streambuf zip_streambuf_type; /** Construct a zip stream * More info on the following parameters can be found in the zlib documentation. */ basic_zip_ostreambase(ostream_reference ostream_, size_t level_, EStrategy strategy_, size_t window_size_, size_t memory_level_, size_t buffer_size_) : m_buf(ostream_, level_, strategy_, window_size_, memory_level_, buffer_size_) { this->init(&m_buf); } /// returns the underlying zip ostream object zip_streambuf_type* rdbuf() { return &m_buf; } /// returns the zlib error state int get_zerr() const { return m_buf.get_zerr(); } /// returns the uncompressed data crc long get_crc() const { return m_buf.get_crc(); } /// returns the compressed data size long get_out_size() const { return m_buf.get_out_size(); } /// returns the uncompressed data size long get_in_size() const { return m_buf.get_in_size(); } private: zip_streambuf_type m_buf; }; /*! \brief A zipper ostream This class is a ostream decorator that behaves 'almost' like any other ostream. At construction, it takes any ostream that shall be used to output of the compressed data. When finished, you need to call the special method close or call the destructor to flush all the intermidiate streams. Example: \code // creating the target zip string, could be a fstream ostringstream ostringstream_; // creating the zip layer zip_ostream zipper(ostringstream_); // writing data zipper<, typename ElemA = std::allocator, typename ByteT = unsigned char, typename ByteAT = std::allocator > class basic_gzip_ostream: public basic_zip_ostreambase, public std::basic_ostream { public: typedef typename std::basic_ostream::char_type char_type; typedef std::basic_ostream& ostream_reference; typedef basic_zip_ostreambase zip_ostreambase_type; typedef std::basic_ostream ostream_type; /** Constructs a zipper ostream decorator * * \param ostream_ ostream where the compressed output is written * \param is_gzip_ true if gzip header and footer have to be added * \param level_ level of compression 0, bad and fast, 9, good and slower, * \param strategy_ compression strategy * \param window_size_ see zlib doc * \param memory_level_ see zlib doc * \param buffer_size_ the buffer size used to zip data When is_gzip_ is true, a gzip header and footer is automatically added. */ basic_gzip_ostream(ostream_reference ostream_, size_t level_ = Z_DEFAULT_COMPRESSION, EStrategy strategy_ = DefaultStrategy, size_t window_size_ = 15, size_t memory_level_ = 8, size_t buffer_size_ = detail::default_buffer_size) : zip_ostreambase_type(ostream_, level_, strategy_, window_size_, memory_level_, buffer_size_), ostream_type(this->rdbuf()), m_closed(false) { add_header(); } ~basic_gzip_ostream() { close(); } void close() { if (m_closed) return; this->flush(); this->rdbuf()->zfinish(); add_footer(); m_closed = true; } private: static void put_long(ostream_reference out_, unsigned int x_); void add_header(); void add_footer(); bool m_closed; }; template, typename ElemA = std::allocator, typename ByteT = unsigned char, typename ByteAT = std::allocator > class basic_zip_ostream: public basic_zip_ostreambase, public std::basic_ostream { public: typedef typename std::basic_ostream::char_type char_type; typedef std::basic_ostream& ostream_reference; typedef basic_zip_ostreambase zip_ostreambase_type; typedef std::basic_ostream ostream_type; /** Constructs a zipper ostream decorator * * \param ostream_ ostream where the compressed output is written * \param is_gzip_ true if gzip header and footer have to be added * \param level_ level of compression 0, bad and fast, 9, good and slower, * \param strategy_ compression strategy * \param window_size_ see zlib doc * \param memory_level_ see zlib doc * \param buffer_size_ the buffer size used to zip data When is_gzip_ is true, a gzip header and footer is automatically added. */ basic_zip_ostream(ostream_reference ostream_, size_t level_ = Z_DEFAULT_COMPRESSION, EStrategy strategy_ = DefaultStrategy, size_t window_size_ = 15, size_t memory_level_ = 8, size_t buffer_size_ = detail::default_buffer_size) : zip_ostreambase_type(ostream_, level_, strategy_, window_size_, memory_level_, buffer_size_), ostream_type(this->rdbuf()), m_closed(false) { } ~basic_zip_ostream() { close(); } void close() { if (m_closed) return; this->flush(); this->rdbuf()->zfinish(); m_closed = true; } private: bool m_closed; }; /// A typedef for basic_zip_ostream typedef basic_gzip_ostream ogzstream; /// A typedef for basic_zip_ostream typedef basic_gzip_ostream wgzostream; /// A typedef for basic_zip_ostream typedef basic_zip_ostream ozstream; /// A typedef for basic_zip_ostream typedef basic_zip_ostream wzostream; } // zstream #include "ozstream_impl.hpp" #endif logserver/src/contrib/ozstream_impl.hpp000066400000000000000000000157271512456611200207320ustar00rootroot00000000000000/* zstream-cpp Library License: -------------------------- The zlib/libpng License Copyright (c) 2003 Jonathan de Halleux. This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution Author: Jonathan de Halleux, dehalleux@pelikhan.com, 2003 Gero Mueller, post@geromueller.de, 2015 */ #ifndef OUTPUT_ZIP_STREAM_IMPL_HPP #define OUTPUT_ZIP_STREAM_IMPL_HPP #include "ozstream.hpp" #include "zlib.h" #include #include #ifndef OS_CODE # define OS_CODE 0x03 /* assume Unix */ #endif namespace zstream { template basic_zip_streambuf::basic_zip_streambuf( ostream_reference ostream_, size_t level_, EStrategy strategy_, size_t window_size_, size_t memory_level_, size_t buffer_size_) : m_ostream(ostream_), m_output_buffer(buffer_size_, 0), m_buffer( buffer_size_, 0), m_crc(0) { m_zip_stream.zalloc = (alloc_func) 0; m_zip_stream.zfree = (free_func) 0; m_zip_stream.next_in = NULL; m_zip_stream.avail_in = 0; m_zip_stream.avail_out = 0; m_zip_stream.next_out = NULL; m_err = deflateInit2(&m_zip_stream, std::min(9, static_cast(level_)), Z_DEFLATED, -static_cast(window_size_), // <-- changed std::min(9, static_cast(memory_level_)), static_cast(strategy_)); m_zip_stream.avail_out = static_cast(m_output_buffer.size()); m_zip_stream.next_out = &(m_output_buffer[0]); char_type *p = &(m_buffer[0]); this->setp(p, p + buffer_size_); } template basic_zip_streambuf::~basic_zip_streambuf() { } template int basic_zip_streambuf::sync() { int c = overflow(EOF); if (c == EOF) return -1; else return 0; } template typename basic_zip_streambuf::int_type basic_zip_streambuf< Elem, Tr, ElemA, ByteT, ByteAT>::overflow( typename basic_zip_streambuf::int_type c) { // buffer full, zip it.. int w = static_cast(this->pptr() - this->pbase()); if (w > 0) { if (zip_to_stream(this->pbase(), w)) { this->setp(this->pbase(), this->epptr()); } else { return EOF; } } // write c if not EOF if (c != EOF) { *this->pptr() = c; this->pbump(1); return c; } else { return 0; } } template std::streamsize basic_zip_streambuf::zip_write( int flag) { std::streamsize total_written_byte_size = 0; m_err = ::deflate(&m_zip_stream, flag); if (m_err == Z_OK || m_err == Z_STREAM_END) { std::streamsize written_byte_size = static_cast(m_output_buffer.size()) - m_zip_stream.avail_out; if (written_byte_size > 0) { total_written_byte_size += written_byte_size; // ouput buffer is full, dumping to ostream m_ostream.write((const char_type*) &(m_output_buffer[0]), static_cast(written_byte_size / sizeof(char_type))); // checking if some bytes were not written. size_t remainder = written_byte_size % sizeof(char_type); if (remainder != 0) { // copy to the beginning of the stream memcpy(&(m_output_buffer[0]), &(m_output_buffer[written_byte_size - remainder]), remainder); } m_zip_stream.avail_out = static_cast(m_output_buffer.size() - remainder); m_zip_stream.next_out = &m_output_buffer[remainder]; } } return total_written_byte_size; } template bool basic_zip_streambuf::zip_to_stream( typename basic_zip_streambuf::char_type* buffer_, std::streamsize buffer_size_) { m_zip_stream.next_in = (byte_buffer_type) buffer_; m_zip_stream.avail_in = static_cast(buffer_size_ * sizeof(char_type)); // updating crc m_crc = ::crc32(m_crc, m_zip_stream.next_in, m_zip_stream.avail_in); do { zip_write(0); } while (m_zip_stream.avail_in != 0 && m_err == Z_OK); return m_err == Z_OK; } template std::streamsize basic_zip_streambuf::zfinish() { std::streamsize total_written_byte_size = 0; this->sync(); // updating crc m_crc = crc32(m_crc, m_zip_stream.next_in, m_zip_stream.avail_in); do { total_written_byte_size += zip_write(Z_FINISH); } while (m_err == Z_OK); m_ostream.flush(); m_err = deflateEnd(&m_zip_stream); return total_written_byte_size; } template void basic_gzip_ostream::put_long( typename basic_gzip_ostream::ostream_reference out_, unsigned int x_) { static const int size_ul = sizeof(unsigned int); static const int size_c = sizeof(char_type); static const int n_end = size_ul / size_c; out_.write(reinterpret_cast(&x_), n_end); } template void basic_gzip_ostream::add_header() { char_type zero = 0; this->rdbuf()->get_ostream().put( static_cast(detail::gz_magic[0])).put( static_cast(detail::gz_magic[1])).put( static_cast(Z_DEFLATED)).put(zero) //flags .put(zero).put(zero).put(zero).put(zero) // time .put(zero) //xflags .put(static_cast(OS_CODE)); } template void basic_gzip_ostream::add_footer() { put_long(this->rdbuf()->get_ostream(), this->rdbuf()->get_crc()); put_long(this->rdbuf()->get_ostream(), this->rdbuf()->get_in_size()); } } // zstream #endif logserver/src/contrib/zstream_common.hpp000066400000000000000000000036251512456611200210740ustar00rootroot00000000000000/* zstream-cpp Library License: -------------------------- The zlib/libpng License Copyright (c) 2003 Jonathan de Halleux. This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution Author: Jonathan de Halleux, dehalleux@pelikhan.com, 2003 Gero Mueller, post@geromueller.de, 2015 */ #ifndef ZIP_STREAM_COMMON_HPP #define ZIP_STREAM_COMMON_HPP #include namespace zstream { /// Compression strategy, see zlib doc. enum EStrategy { StrategyFiltered = 1, StrategyHuffmanOnly = 2, DefaultStrategy = 0 }; namespace detail { /// default gzip buffer size, /// change this to suite your needs const size_t default_buffer_size = 4096; const int gz_magic[2] = { 0x1f, 0x8b }; /* gzip magic header */ /* gzip flag byte */ const int gz_ascii_flag = 0x01; /* bit 0 set: file probably ascii text */ const int gz_head_crc = 0x02; /* bit 1 set: header CRC present */ const int gz_extra_field = 0x04; /* bit 2 set: extra field present */ const int gz_orig_name = 0x08; /* bit 3 set: original file name present */ const int gz_comment = 0x10; /* bit 4 set: file comment present */ const int gz_reserved = 0xE0; /* bits 5..7: reserved */ } // detail } // zstream #endif logserver/src/curses_wrapper.h000066400000000000000000000053171512456611200171030ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __CURSES_WRAPPER__H__ #define __CURSES_WRAPPER__H__ #include #include using namespace std; /* A wrapper around curses functions to make them threadsafe */ class CursesWrapper { public: /* static mutex holder */ static inline mutex& get_mutex() { static mutex _m; return _m; } /* simulate blocking getch by polling for characters */ static int getch_blocking() { while (true) { unique_lock ul(get_mutex()); nodelay(stdscr, TRUE); int ret = getch(); if (ret != ERR) return ret; ul.unlock(); this_thread::sleep_for(chrono::milliseconds(3)); } } /* calls getch without blocking */ static int getch_nonblocking() { unique_lock ul(get_mutex()); nodelay(stdscr, TRUE); return getch(); } /* puts the current cursor yx coords into parms */ static void get_yx(size_t* y, size_t* x) { unique_lock ul(get_mutex()); *y = getcury(stdscr); *x = getcurx(stdscr); } /* moves to the yx position in the parms */ static void move_yx(int y, int x) { unique_lock ul(get_mutex()); wmove(stdscr, y, x); } /* for an attr defined in the paramter, turns it on or off depending on * the boolean second parameter */ static int attr(NCURSES_ATTR_T& attr, bool on) { unique_lock ul(get_mutex()); if (on) return wattron(stdscr, attr); return wattroff(stdscr, attr); } /* clears to the end of the line */ static int clear_eol() { unique_lock ul(get_mutex()); return wclrtoeol(stdscr); } /* start of window work goes here */ static void start_win() { } /* locks ncurses and calls end win */ static int end_win() { unique_lock ul(get_mutex()); return endwin(); } /* puts the char ch at position yx */ static int add_ch(int y, int x, const chtype ch) { unique_lock ul(get_mutex()); if (wmove(stdscr, y, x) == ERR) return ERR; return waddch(stdscr, ch); } /* refreshes the screen */ static void refresh() { unique_lock ul(get_mutex()); ::refresh(); } }; #endif // __CURSES_WRAPPER__H__ logserver/src/directory_line_provider.h000066400000000000000000000063041512456611200207610ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __DIRECTORY_LINE_PROVIDER__H__ #define __DIRECTORY_LINE_PROVIDER__H__ #include #include #include #include #include #include "base_line_provider.h" #include "i_log_lines.h" using namespace std; /* directory line provider is used to display media files in a directory to * allow logserver to function like a media server */ class DirectoryLineProvider : public BaseLineProvider { public: DirectoryLineProvider(ILogLines* ll, const string& dir) : BaseLineProvider(ll), _dir(dir) {} virtual ~DirectoryLineProvider() {} virtual string get_line(size_t pos) override { if (pos >= _files.size()) return ""; return _files.at(pos); } protected: /* load the files in the passed directory recursively and put an 'x' * next to the ones that have an entry in the mediaserver file */ void loader() override { set sorted; for (const auto& entry : filesystem::recursive_directory_iterator(_dir)) { sorted.insert(entry.path()); } for (const auto& x : sorted) { process(x); } ifstream fin(_dir + "/.mediaserver"); string s; while (getline(fin, s)) { if (_file_to_index.count(s)) { _display[_file_to_index[s]][0] = 'x'; } } commit(); } void commit() { for (const auto& x : _display) { add_line(x); } } void process(const string& file) { // TODO: only in mediaserver mode if (!is_media(file)) return; _file_to_index[file] = _files.size(); _files.emplace_back(file); string line = tidy(file); _display.emplace_back(tidy(file)); } static string tidy(const string& file) { string ret = " "; bool off = false; optional last; for (size_t i = 0; i < file.size(); ++i) { if (file[i] == '[') off = true; else if (file[i] == ']') off = false; else if (!off) { if (last && (*last == '/' || punctuation(*last)) && file[i] == ' '); //nop else if (punctuation(file[i])) ret += ' '; else ret += file[i]; last = file[i]; } } return ret; } static bool punctuation(char c) { static set chars = {'.', '-', '_', ' '}; return chars.count(c); } static bool is_media(const string& file) { size_t n = file.size(); if (n < 4) return false; if (file.substr(n - 4) == ".mkv") return true; if (file.substr(n - 4) == ".avi") return true; if (file.substr(n - 4) == ".mp4") return true; return false; } string _dir; vector _files; vector _display; unordered_map _file_to_index; }; #endif // __DIRECTORY_LINE_PROVIDER__H__ logserver/src/epoch.h000066400000000000000000000037711512456611200151370ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __EPOCH__H__ #define __EPOCH__H__ #include #include #include "constants.h" using namespace std; /* stores the current epoch. This is used in rendering, so whenever the state * changes the renderer starts computing the lines to show for a particular * epoch. if that render doesn't finish before the epoch advances those lines * are not displayed */ class Epoch { public: // start at epoch 1, so at the start renderers can pretend they've // successfully rendered epoch 0 Epoch() : _epoch(1) {} virtual ~Epoch() { } // signal anyone waiting on the epoch advance to wake up, since we are // shuting down virtual void shutdown() { _epoch = G::NO_POS; _cond.notify_all(); } // increment the epoch and notify listeners. virtual inline size_t advance() { if (_epoch != G::NO_POS) { ++_epoch; } _cond.notify_all(); return _epoch; } // wait until epoch advances template inline void wait(T* ul) const { if (_epoch == G::NO_POS) return; _cond.wait(*ul); } // returns the current epoch virtual inline size_t cur() const { return _epoch; } protected: // value of epoch atomic _epoch; // condition waiting on epoch change mutable condition_variable_any _cond; // mutex for threadsafety mutex _m; }; #endif // __EPOCH__H__ logserver/src/event_bus.h000066400000000000000000000074441512456611200160340ustar00rootroot00000000000000#ifndef __EVENT_BUS__H__ #define __EVENT_BUS__H__ #include #include #include #include #include #include #include "i_log_lines_events.h" using namespace std; class LogLines; /* EventBus manages event reporting that can effect separate components. In * particular, when new lines are inserted, added, or deleted from the loglines, * things like the filter runner's matches and pinned lines need to adjust */ class EventBus : public ILogLinesEvents { public: // singleton design static EventBus* _() { static EventBus _event_bus; return &_event_bus; } protected: // disallow instances EventBus() {} virtual ~EventBus() { assert(empty(&_ll_events)); } public: // add target to the broadcast list for src virtual void enregister(const LogLines* src, ILogLinesEvents* target) { unique_lock ul(_m); assert(!_ll_events[src].count(target)); _ll_events[src].insert(target); } // erases target wherever it exists virtual void deregister(ILogLinesEvents* target) { unique_lock ul(_m); for (auto& x : _ll_events) { x.second.erase(target); } } // erases target in the broadcast list for src virtual void deregister(const LogLines* src, ILogLinesEvents* target) { unique_lock ul(_m); // broadcaster deregistered if (!_ll_events.count(src)) return; assert(_ll_events[src].count(target)); _ll_events[src].erase(target); } // an existing line was changed virtual void edit_line(LogLines* ll, size_t pos, Line* line) override { shared_lock ul(_m); if (!_ll_events.count(ll)) return; for (const auto& x : _ll_events[ll]) { x->edit_line(ll, pos, line); } } // a new line was added virtual void append_line(LogLines* ll, size_t pos, Line* line) override { shared_lock ul(_m); if (!_ll_events.count(ll)) return; for (const auto& x : _ll_events[ll]) { x->append_line(ll, pos, line); } } // an insertion event virtual void insertion(LogLines* ll, size_t pos, size_t amount) override { shared_lock ul(_m); if (!_ll_events.count(ll)) return; for (const auto& x : _ll_events[ll]) { x->insertion(ll, pos, amount); } } // a deletion event virtual void deletion(LogLines* ll, size_t pos, size_t amount) override { shared_lock ul(_m); if (!_ll_events.count(ll)) return; for (const auto& x : _ll_events[ll]) { x->deletion(ll, pos, amount); } } // clear all lines virtual void clear_lines(LogLines* ll) override { shared_lock ul(_m); if (!_ll_events.count(ll)) return; for (const auto& x : _ll_events[ll]) { x->clear_lines(ll); } } // announce that a loglines no longer will make events void eventmaker_finished(LogLines* src) { unique_lock ul(_m); _ll_events.erase(src); } /* bool function to use in an assert on destructors of classes that * register for event busses to make sure they unregistered */ bool present(ILogLinesEvents* dst) { shared_lock ul(_m); if (present(&_ll_events, dst)) return true; return false; } protected: /* template helper to implement present check */ template bool present(map>* m, R dst) { for (const auto& x : *m) { for (const auto& y: x.second) { if (y == dst) return true; } } return false; } /* template helper to implement empty check during destruction */ template bool empty(map>* m) { shared_lock ul(_m); for (const auto& x : *m) { if (x.second.size()) return false; } return true; } shared_mutex _m; map> _ll_events; }; #endif // __EVENT_BUS__H__ logserver/src/exit_signal.h000066400000000000000000000023671512456611200163470ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __EXIT_SIGNAL__H__ #define __EXIT_SIGNAL__H__ #include using namespace std; /* global static class to signal to all threads whether we are shutting down */ class ExitSignal { public: /* returns true if the exit signal is set. if the parameter is true then * it will set the signal to true, otherwise it leaves its existing * value */ static bool check(bool set) { static bool exit_signal = false; static mutex m; unique_lock ul(m); if (set) exit_signal = true; return exit_signal; } }; #endif // __EXIT_SIGNAL__H__ logserver/src/explored_range.h000066400000000000000000000371121512456611200170330ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __EXPLORED_RANGE__H__ #define __EXPLORED_RANGE__H__ #include #include #include #include #include "config.h" #include "constants.h" using namespace std; /* This class manages searching results. When logserver is handling huge files, * searching can take a while. Instead of doing it linearlly, it uses the * current position in navigation to shoot search probes in both directions. As * the user moves, this can cause more searched and unsearched areas. This * manages where those overlap and collapes them. Explored range is not * threadsafe, assumes user handles threading. */ class ExploredRange { public: ExploredRange(size_t range) : _range(range), _dir(false) {} virtual ~ExploredRange() {} /* loglines was cleared. reset the explored range to no findings and all * future lines will be matched in tail mode */ virtual void clear() { _findings.clear(); _explored.clear(); _explored[0] = G::EOF_POS; _end = 0; } /* dump out explored segments to ostream os */ virtual void trace(ostream& os) { os << "tot=" << _explored.size() << " "; for (auto & x : _explored) { os << "(" << x.first << " -> " << x.second << ") "; } } /* This inserts findings even if the range has not been explored * according to the range search. In practice this is not likely to * happen, since any insertion would involve setting the whence to this * position and exploring it. */ virtual void add_finding(size_t pos) { _findings.insert(pos); } /* This removes a finding from a pos, e.g., because the line was edited * to no longer match the search */ virtual void remove_finding(size_t pos) { _findings.erase(pos); } // occurs when loglines inserts new elements in the middle, e.g., // because a long line is broken virtual void insert_or_delete_lines(size_t pos, size_t amount, bool insert) { /* TODO: right now deletions are only size one (or wholesale * clear). If that changes, we need to handle the case where a * deletion occurs outside a range, but eats into some of all of * the next range, or spans two distinct ranges */ assert(insert || amount == 1); set replace; // handle the existing findings for (auto &x : _findings) { // keep findings prior to event pos if (x < pos) replace.insert(x); else { // insert push back by amount if (insert) replace.insert(x + amount); // deletions within [pos, pos+amount) are // removed, otherwise pulled by amount else if (x >= pos + amount) { replace.insert(x - amount); } } } _findings = std::move(replace); // if explored map is no longer relevant no need to update the // indices of all the searched areas if (completed()) return; map explore; // TODO: can put each range handling as its own function for (const auto &x : _explored) { if (x.second <= pos) { // prior to insertions uneffected explore[x.first] = x.second; } else if (x.first >= pos) { // segments after insertion are pushed unless // EOF already size_t idx; if (insert) idx = x.first + amount; else { assert(amount <= x.first); idx = x.first - amount; } if (x.second != G::EOF_POS) { if (insert) { explore[idx] = x.second + amount; } else { explore[idx] = x.second - amount; } } else { // adjust position but keep eof pos explore[idx] = G::EOF_POS; } } else { // insertion or deletion happened within a range // grow only the end of the range unless EOF // already assert(x.first < pos && x.second > pos); if (x.second != G::EOF_POS) { if (insert) { explore[x.first] = x.second + amount; // a deletion could break out the range } else if (amount <= x.second - x.first) { explore[x.first] = x.second - amount; } else { /* TODO: with multiple delete, * this will trigger. now it can * only happen if there is * nothing explored, a * placeholder is held, and the * deletion will anyways make * the received results ignored. */ assert(x.second == x.first); explore[x.first] = x.second; } } else { // keep explored range explore[x.first] = x.second; } } } _explored = std::move(explore); } /* given the current line user is look at in whence, provides search * range of lines in [start, end] to look for matches as output * variables. If end < start, it means search upwards. Alternates the * search direction each time */ virtual void explore(size_t whence, size_t* start, size_t* end) { *start = 0; *end = 0; // step 1. determine if whence is already within a range we have // explored, or if we have to insert a new position in the // _explored map. bool insert = false; // either it->first == whence, > whence, or it is at end auto it = _explored.lower_bound(whence); // insert is true if whence is not part of any segment in // _explored, either before the first start, or between some // [end, start] sequence. if (it == _explored.end() || it->first != whence) { // if _explored is empty, or first element is past // whence, then we have to insert if (it == _explored.begin()) { insert = true; } else { // otherwise we can go back, check if whence is // part of the previous range, if not then // insert --it; if (it->second < whence) insert = true; } } // step 2. insert if we have to. revalidate the iterator after // insertion if (insert) { _explored[whence] = whence; it = _explored.find(whence); } assert(it != _explored.end()); assert(_explored.size()); // step 3. it now points to a valid element in _explored. // alternate directions and grow out its explored range // accounting for other elements in _explored that it will // overlap _dir = !_dir; // if we start at an endpoint, configure the direction if (it->first == 0) _dir = true; if (_end && it->second >= *_end) _dir = G::DIR_UP; if (!_dir) { // search up *start = it->first; if (*start < _range) { *end = 0; } else { *end = *start - _range; } if (it != _explored.begin()) { --it; if (it->second > *end) *end = it->second; } } else { // search down *start = it->second; *end = *start + _range; ++it; if (it != _explored.end()) { if (it->first < *end) *end = it->first; } else if (_end != nullopt) { // if we have an _end marker, all new lines // would be searched at the time of insertion // into loglines, so no need to search if (*end > *_end) *end = *_end; } } } // mark the last element we need to search for based on the size of the // log right now. all subsequent matches will be determined at the time // of insertion virtual void mark_end(size_t end) { _end = end; /* inserts a new explored range from the marked end to a pseudo * max loglines size to simplify logic regarding gaps between * positions and explored ranges across all keywords */ auto it = _explored.end(); if (_explored.size() && (--it)->second == end) { it->second = G::EOF_POS; } else { _explored[end] = G::EOF_POS; } } /* returns the percent that this keyword has done searching */ virtual int percent() { assert(_end); size_t done = 0; size_t total = *_end; for (const auto& x : _explored) { if (x.second == G::EOF_POS) { done += (*_end - x.first); } else { done += (x.second - x.first); } } return (done * 100 / total); } // log lines follower has found a new match in an new line, add it at // the end of _findings. Usually due to streaming data, but can also // occur when user hits break on a line, so insert with end() hint when // it is past the _end pos. virtual void post_end(size_t pos) { assert(_end != nullopt); if (pos > *_end) [[likely]] _findings.insert(_findings.end(), pos); else [[unlikely]] _findings.insert(pos); } // return true if our search is complete, becuase there is a single // element in explored from 0->_end virtual bool completed() { // loglines has been cleared, everything is matched in tail mode if (_end && !*_end) return true; // if there are more than one range there will be a gap between // them if (_explored.size() != 1) return false; auto it = _explored.begin(); // there is one range with a gap before if ((it->first) != 0) return false; // range has sentinel value indicating full exploration if (it->second == G::EOF_POS) { assert(_end); return true; } // there is one range with a gap after return false; } /* we have results back after exploring the range start to end. * based on how explore assigns start / end we know there is an element * in _explored with either key==start or value==start. find it and * extend the correct direction, then check if it bumps against another * element and merge them */ virtual void extend(size_t start, size_t end, const set& results) { assert(_explored.size()); assert(start != end); for (auto & x : results) { _findings.insert(x); } // it is possible we assigned a search before the _end got // marked, but it got marked before the search finished. // in this case ignore the results past _end since // we'll get those through insert follower if (_end != nullopt) { if (start >= *_end) start = *_end; if (end >= *_end) end = *_end; } // step 1. find the relevant element in _explored auto it = _explored.lower_bound(start); if (it == _explored.end() || it->first != start) --it; // step 2. it is now valid and equal to the key or value for // some elemented in _explored (or equal to both). depending on // the direction we explored, assert start is in the correct // position of it and extend the range. if (start < end) { assert(it->second == start); it->second = end; ++it; } else { assert(it->first == start); _explored[end] = it->second; _explored.erase(it); it = _explored.find(end); assert(it != _explored.end()); } // step 3. check if our extension allows two adjacent elements // in _explored to be merged. it now points to the element whose // new explored range is earlier, so if it is either the // beginning or the invalid end element then it meant there was // no element it could bump into if (it != _explored.begin() && it != _explored.end()) { auto two = it; --it; // we have covered the gap if (it->second == two->first) { size_t pos = it->first; it->second = two->second; _explored.erase(two); it = _explored.lower_bound(pos); } } } /* returns true if the line at position pos is a match */ virtual inline bool is_match(size_t pos) { return _findings.count(pos); } /* returns the position of next matching line given a starting point in * whence and a direction to look. It returns G::NO_POS if there is no * finding in that direction past whence, or if there is a gap in the * searched ranges between whence and the result */ virtual inline size_t next_match(size_t whence, bool dir) { if (dir) { auto it = _findings.lower_bound(whence); if (it == _findings.end()) return G::NO_POS; if (gapped(*it, whence)) return G::NO_POS; return *it; } else { auto it = _findings.lower_bound(whence); if (it == _findings.begin()) return G::NO_POS; --it; if (gapped(*it, whence)) return G::NO_POS; return *it; } } /* returns the position where exploration is gap-free from parameter * whence in direction parameter dir. If whence is not in an explored * range returns G::NO_POS */ virtual inline size_t next_range(size_t whence, bool dir) { return next_range_locked(whence, dir); } /* inserts all the findings we have into the set parameter lines, i.e., * doing an OR search on matching lines */ virtual void disjunctive_join(set* lines) const { set ret; set_union(lines->begin(), lines->end(), _findings.begin(), _findings.end(), inserter(ret, ret.begin())); lines->swap(ret); } /* replaces input set parameter lines with only those lines that also * appear in our findings, i.e., an AND search on matching lines */ virtual void conjunctive_join(set* lines) { set ret; set_intersection(lines->begin(), lines->end(), _findings.begin(), _findings.end(), inserter(ret, ret.begin())); lines->swap(ret); } protected: /* returns true if there is a gap between pos1 and pos2, so that we * don't display results beyond the gap. because follow up lines can * appear as findings but aren't representing explored ranges we cannot * be sure pos1 is in an explored range */ virtual inline bool gapped(size_t pos1, size_t pos2) { if (pos1 == pos2) return false; auto it = _explored.lower_bound(pos1); if (it == _explored.end() || it->first != pos1) { if (it == _explored.begin()) return true; --it; } if (!is_within(it->first, pos1, it->second)) return true; return !is_within(it->first, pos2, it->second); } /* returns true if start <= val < end, i.e., val is in the searched * segment [start, end] */ static inline bool is_within(size_t start, size_t val, size_t end) { return (start <= val && val < end); } /* returns G::NO_POS if whence is not inside a searched segment. * Otherwise it finds the segment it is in and returns the start/end of * it based on direction */ virtual inline size_t next_range_locked(size_t whence, bool dir) { auto it = _explored.upper_bound(whence); // first explored segment is after range if (it == _explored.begin()) return G::NO_POS; --it; // whence is after a segment if (it->second < whence) return G::NO_POS; assert(it->first <= whence); assert(whence <= it->second); if (dir) return it->second; else return it->first; } // map of all the searched segments in loglines. empty map means we // havn't search anything, otherwise it consists of [start, end) pairs // indicating all positions from start to end-1 have been looked at. // gaps occur when searching is done from different places and continues // until the entire map is a single element from [0, loglines size). map _explored; // TODO: on copy, invalidate findings if its within a blocksize and // validate on those findings instead of restarting the search // the set of matches we've found by searching set _findings; // when issuing searching tasks, the number of elements to search for. // this is to prevent time wasted on locking and unlocking loglines, as // well as holding a loglines lock for too long size_t _range; // the direction that we last searched, so that we can alternate going // up and down if we are at some particular line and the search takes a // while bool _dir; // stores the number of lines of loglines so we know when we are done // searching. while new lines can appear in loglines for streaming // input, when _end is set we require that those lines are processed as // they are added instead of being searched for later optional _end; }; #endif // __EXPLORED_RANGE__H__ logserver/src/fd_line_provider.h000066400000000000000000000057601512456611200173530ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __FD_LINE_PROVIDER__H__ #define __FD_LINE_PROVIDER__H__ #include #include #include #include #include #include "base_line_provider.h" #include "config.h" #include "i_log_lines.h" using namespace std; /* Wraps a file descriptor. Continually reads from the fd pushing new lines to * the log lines. Uses select to avoid blocking and monitors the exit condition * to quit. */ class FDLineProvider : public BaseLineProvider { public: FDLineProvider(ILogLines* ll, int fd) : BaseLineProvider(ll), _fd(fd), _len(1<<12) {} virtual ~FDLineProvider() { close(_fd); } protected: // reads from the fd in a non blocking way. if there is data available, // separate it into lines and add those lines to the loglines. virtual void loader() override { _buf.reset(new char[_len + 1]); char* buf = _buf.get(); stringstream remain_ss; bool first = true; bool remain = false; fcntl(_fd, F_SETFL, O_NONBLOCK); bool ret = true; fd_set fds; struct timeval tv; while (!exit()) { FD_ZERO(&fds); FD_SET(_fd, &fds); tv.tv_sec = 0; tv.tv_usec = 100000; int ready = select(_fd + 1, &fds, nullptr, nullptr, &tv); if (exit()) return; if (ready != 1) continue; ssize_t r = ::read(_fd, buf, _len); if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { first = false; continue; } else if (r <= 0) { if (remain) ret = queue_line(remain_ss.str()); if (exit() || !ret) return; eof(); break; } assert(r > 0); size_t data_available = static_cast(r); ssize_t start = 0; for (size_t i = 0; i < data_available; ++i) { if (buf[i] == '\n') { buf[i] = '\0'; if (!first) unset_eof(); if (start == 0 && remain) { remain_ss << buf; ret = queue_line(remain_ss.str()); if (!ret) return; remain_ss.str(""); remain = false; } else { ret = queue_line(buf + start); if (!ret) return; } start = i + 1; } } flush_lines(); if (start < r) { buf[r] = '\0'; remain_ss << buf + start; remain = true; } } flush_lines(); } // the file decriptor we are serving int _fd; // length of the buffer size, e.g., 4 KiB size_t _len; // the reading buffer unique_ptr _buf; }; #endif // __FD_LINE_PROVIDER__H__ logserver/src/file_line_provider.h000066400000000000000000000054201512456611200176720ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __FILE_LINE_PROVIDER__H__ #define __FILE_LINE_PROVIDER__H__ #include #include "base_line_provider.h" #include "i_log_lines.h" #include "run.h" #include "zcat.h" #include "contrib/izstream.hpp" using namespace std; /* a line provider based around a file. takes a file name and reads it, produces * its contents as lines to the LogLines. when it gets to the end of the file, * it signals eof() to loglines. it keeps tailing the file though and if new * lines come it unsets the eof() and keeps adding them */ class FileLineProvider : public BaseLineProvider { public: FileLineProvider(ILogLines* ll, const string& filename) : BaseLineProvider(ll), _filename(filename) {} virtual ~FileLineProvider() {} protected: void loader() override { /* the file being read */ unique_ptr basefile; /* the stream being read in case we are decompressing the file */ unique_ptr fin; fin.reset(new ifstream(_filename)); /* read the file line of the file. if it is a gzip file, reopen * with gzip reader. otherwise reset to the top */ string line; getline(*fin.get(), line); if (ZCat::magic(line)) { basefile.reset(new ifstream(_filename)); fin.reset(new zstream::igzstream(*basefile.get())); } else { fin->clear(); fin->seekg(0, ios::beg); } bool first_run = true; // counter of number of times we didn't get any new lines int num_checks = 0; while (!exit()) { while (!exit() && getline(*fin.get(), line)) { queue_line(line); if (!first_run) unset_eof(); num_checks = 0; } flush_lines(); /* the first time we think we are EOF believe it. if we * are wrong then continue to be skeptical */ if (first_run) eof(); first_run = false; if (num_checks == 100) { // 10s passed without updates. lets believe eof // again. eof(); // turn of checking ++num_checks; } else if (num_checks < 100) { ++num_checks; } this_thread::sleep_for(chrono::milliseconds(100)); fin->clear(); } } /* the file we are serving */ string _filename; }; #endif // __FD_LINE_PROVIDER__H__ logserver/src/filter_manager.h000066400000000000000000000420201512456611200170060ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __FILTER_MANAGER__H__ #define __FILTER_MANAGER__H__ #include #include #include #include #include #include "config.h" #include "constants.h" #include "format_string.h" #include "i_log_lines_events.h" #include "log_lines.h" #include "line_filter_keyword.h" #include "navigation.h" #include "pin_manager.h" #include "render_queue.h" #include "renderer.h" using namespace std; /* FilterManager stores the line filter keywords for the current view and * manages growing the context window */ class FilterManager : public ILogLinesEvents { public: /* construct with loglines and navi for the filter manager */ FilterManager(LogLines* ll, Navigation* navi) : _current_keyword(nullptr), _ll(ll), _navi(navi), _gentle_and(true), _context_length(0), _display_length(G::NO_POS), _keyword_string_updating(true) { unique_lock ul(_mutex); _display_length_future = async(&FilterManager::compute_length, this); _length_result.resize(G::FILTER_TOTAL); EventBus::_()->enregister(_ll, this); } // default destructor virtual ~FilterManager() { EventBus::_()->deregister(_ll, this); } /* event bus functions. just invalidate length result and force them to * recompute */ virtual void clear_lines([[maybe_unused]] LogLines* ll) override { _clear_results = true; _pins.clear(); } virtual void edit_line([[maybe_unused]] LogLines* ll, [[maybe_unused]] size_t pos, [[maybe_unused]] Line* line) override { _clear_results = true; } virtual void append_line([[maybe_unused]] LogLines* ll, [[maybe_unused]] size_t pos, [[maybe_unused]] Line* line) override { _clear_results = true; } virtual void insertion([[maybe_unused]] LogLines* ll, size_t pos, size_t amount) override { _clear_results = true; _pins.insertion(pos, amount); } virtual void deletion([[maybe_unused]] LogLines* ll, [[maybe_unused]] size_t pos, [[maybe_unused]] size_t amount) override { _clear_results = true; } // if the screen size was sufficiently big, this sets lines to be all // the positions that we would render at this moment based on our filter // status. virtual void get_view(set* lines) { unique_lock ul(_mutex); get_view_locked(lines); } /* gets the length of the lines that we would display. For no filter it * is just length, but for a search it is the number of results. * Implemented as a future since it can take time to compute and we do * not want to delay rendering for this */ virtual size_t length() { unique_lock ul(_mutex); future_status status = future_status::deferred; if (_display_length_future.valid()) { ul.unlock(); status = _display_length_future.wait_for( chrono::milliseconds(1)); ul.lock(); } if (status == future_status::ready) { _display_length = _display_length_future.get(); } return _display_length; } /* Called when the user hits enter on the current typing of a keyword. * Tell the keyword we are locked in and add it to the list we search * for. Parameter accepted means whether user ended with enter. */ virtual void finish_match(bool accepted) { unique_lock ul(_mutex); _keyword_string = nullopt; // pop keyword if appropriate string keyword = _current_keyword->get_keyword(); if (!accepted || keyword.empty()) { pop_keyword_locked(); return; } // handle new keyword _clear_results = true; _current_keyword->finish(); _keyword_vals.push_back(G::to_lower( _current_keyword->get_keyword())); _current_keyword = nullptr; _display_length_future = async(&FilterManager::compute_length, this); } virtual const FormatString& keyword_string() { unique_lock ul(_mutex); if (!_keyword_string || _keyword_string_updating) { _keyword_string_updating = false; _keyword_string = FormatString(); int colour = 0; for (const auto& x : _keywords) { string val = '[' + x->get_description() + "] "; // the string is volatile, keep recomputing it if (!x->completed()) _keyword_string_updating = true; _keyword_string->add( val, Colour::keyword_number_to_colour(colour)); ++colour; } } return *_keyword_string; } virtual string filter_string() const { unique_lock ul(_mutex); string ret; auto it = _keywords.begin(); if (it == _keywords.end()) return ret; ret += (*it)->get_description(); ++it; while (it != _keywords.end()) { ret += mode_char() + (*it)->get_description(); ++it; } return ret; } // returns the string that the user has currently typed in the context // of searching for a new keyword. virtual void current_type(const string& typed) { unique_lock ul(_mutex); _clear_results = true; assert(_current_keyword); _current_keyword->current_type(typed); } // remove the last keyword on our list. This can be because the user is // removing it deliberatly, or they ended their match with escape // instead of enter virtual bool pop_keyword() { unique_lock ul(_mutex); return pop_keyword_locked(); } /* start a new search. not_inverted means normal search, otherwise a * reverse (remove) search, anchor can be left, right, or none for where * to search on the line, and whence signals to the line filter keyword * where to start searching in the loglines so that matches are promptly * displayed to the user. */ virtual void start_match(int match_parms, size_t whence) { unique_lock ul(_mutex); _clear_results = true; assert(!_current_keyword); _current_keyword = new LineFilterKeyword( *_ll, match_parms, whence); _keywords.push_back(nullptr); _keywords.back().reset(_current_keyword); // automatically switch modes. First the first keyword switch to // and, and for the second, switch to or if its the first time // doing that since 0 keywords. This is because the user has not // specifically seleted AND search yet. If they do to go AND // after and go back to two, keep it in AND mode. if (_keywords.size() == 1) set_mode_conjunction(); else if (_filter_keywords == G::FILTER_AND && _gentle_and && _keywords.size() == 2) { set_mode_disjunction(); _gentle_and = false; } } // returns the string for the current keyword virtual string current_keyword() { unique_lock ul(_mutex); assert(_current_keyword); return _current_keyword->get_keyword(); } // handle tab to switch between or, and, and none modes virtual void toggle_mode() { unique_lock ul(_mutex); _filter_keywords = (_filter_keywords + 1) % (!multiple() ? 2 : 3); _display_length_future = async(&FilterManager::compute_length, this); return; } // returns true if filter mode is set to NONE virtual bool is_mode_all() const { unique_lock ul(_mutex); return _filter_keywords == G::FILTER_NONE; } // string to display on the status bar that names the mode virtual string mode_string() const { unique_lock ul(_mutex); if (_filter_keywords == G::FILTER_NONE) return "ALL"; // no filter if (_keywords.size() == 0) return "TAG"; // only tagged / pinned lines if (_keywords.size() == 1) return "MATCH"; // only one search term if (_filter_keywords == G::FILTER_AND) return "AND"; // AND mode if (_filter_keywords == G::FILTER_OR) return " OR"; // OR mode assert(0); return "NO SUCH MODE"; } /* Searches the current line from the current tab position forwards to * the end of the line, and returns the position of the next * matching keyword on that line, among all possible keywords. Useful in * finding matches on very long lines without breaking them */ virtual size_t find_next_match() const { unique_lock ul(_mutex); size_t cur = _navi->cur(); size_t tab = _navi->tab(); size_t ret = string::npos; for (const auto& x : _keyword_vals) { size_t i = _ll->find(cur, tab + G::h_shift(), x); if (i < ret) ret = i; } if (ret == string::npos) return tab; return ret; } /* Searches the current line backwards from the current tab position for * the first matching keyword among all possible. */ virtual size_t find_prev_match() const { unique_lock ul(_mutex); size_t cur = _navi->cur(); size_t tab = _navi->tab(); size_t ret = string::npos; if (tab == 0) return 0; // TODO: instead we can get the logline once, lower it once, and // then search the keywords for (const auto& x : _keyword_vals) { size_t i = _ll->rfind(cur, tab - 1, x); assert(i == string::npos || i < tab); // if we have a match and the current match is // either not set or we have a closer one, use it if (i != string::npos && (i > ret || ret == string::npos)) { ret = i; } } if (ret == string::npos) return tab; return ret; } /* increments the number of context lines */ virtual void add_context() { ++_context_length; } /* decrements the number of context lines */ virtual void remove_context() { if (_context_length) --_context_length; } /* returns the total number of context lines */ virtual size_t total_context() const { return _context_length; } /* implements shift-up and shift-down in the various modes. * For NONE, goes in direction dir to the next line that has a matching * keyword. * For AND just goes in dir * For OR, goes in dir to the next line that does not contain a keyword * already matching on this line. This allows there to be huge numbers * of one keyword and few of another with large chunks of the first * keyword easily skipped */ virtual size_t keyword_slide(int dir) { unique_lock ul(_mutex); size_t length = _ll->length(); size_t pos = _navi->cur(); size_t use_pos = dir ? pos + 1 : pos; if (!use_pos) return pos; if (use_pos == G::NO_POS) { pos = length; use_pos = pos; } if (_filter_keywords == G::FILTER_AND) return _pins.pinsider( use_pos, dir, SearchRange::keyword_conjunction( pos, dir, _keywords, length)); // if we are in disjuction, we this would just go to next line. // instead go to next lines that isn't matching the same keywork if (_filter_keywords == G::FILTER_OR) { // step 1. find all the keywords not present in current // line set notpresent; // TODO: why is this vals here for (size_t i = 0; i < _keywords.size(); ++i) { if (!_keywords.at(i)->is_match(pos)) { notpresent.insert(_keywords.at(i).get()); } } // step 2. search for the nearest line with any of those // keywords not on the current line if (!notpresent.empty()) { return _pins.pinsider( use_pos, dir, SearchRange::keyword_disjunction( use_pos, dir, G::set_to_vec(notpresent), length)); } } // in disjuction or all, return next matching line return _pins.pinsider(use_pos, dir, SearchRange::keyword_disjunction( use_pos, dir, _keywords, length)); } /* fills out the RenderParms structure with the relevant information * contained in this class */ virtual void set_render_parms(RenderParms* rp) { unique_lock ul(_mutex); rp->context_length = _context_length; rp->filter_keywords = _filter_keywords; for (const auto& x : _keywords) { x->set_whence(_navi->cur()); rp->keywords.push_back(x.get()); } rp->pins = &_pins; } /* gets the closest pins up and down */ virtual pair, optional> nearest_pins( size_t whence) { return make_pair(_pins.nearest_pin(whence, G::DIR_UP), _pins.nearest_pin(whence, G::DIR_DOWN)); } /* turns on a pin at a position */ virtual void set_pin(size_t pos) { _pins.set_pin(pos); } /* toggles a pin at a position */ virtual void toggle_pin(size_t pos) { _pins.toggle_pin(pos); } protected: // returns a char separator for keywords when making a permafilter // loglines. uses & for AND and | for OR. virtual char mode_char() const { assert(_keywords.size() >= 2); if (_filter_keywords == G::FILTER_AND) return '&'; if (_filter_keywords == G::FILTER_OR) return '|'; assert(0 && "permafilter with invalid mode"); return '?'; } /* we are rejecting the current type or removing top keyword in the stack */ virtual bool pop_keyword_locked() { _keyword_string = nullopt; _clear_results = true; if (_keywords.empty()) return false; string word = static_cast(*_keywords.back()); // pop keyword_vals if it matches the line filter keyword we // will pop if (_keyword_vals.empty() || _keyword_vals.back() != G::to_lower(_keywords.back()->get_keyword())) { assert(_current_keyword != nullptr); _current_keyword = nullptr; } else { assert(_current_keyword == nullptr); _keyword_vals.pop_back(); } _keywords.pop_back(); if (empty()) { set_mode_none(); _gentle_and = true; } else if (_keywords.size() == 1) { if (_filter_keywords == G::FILTER_OR) { set_mode_conjunction(); } } return true; } /* can we memoize this when the tab changes */ virtual size_t compute_length() { unique_lock ul(_mutex); for (const auto& x : _keywords) { if (!x->completed()) { _clear_results = true; break; } } if (_clear_results) { for (size_t i = 0; i < _length_result.size(); ++i) { _length_result[i] = nullopt; } _clear_results = false; } if (_length_result.at(_filter_keywords) == nullopt) { size_t ret = G::NO_POS; if (_filter_keywords == G::FILTER_NONE) { ret = _ll->length(); } else { set lines; get_view_locked(&lines); ret = lines.size(); } _length_result[_filter_keywords] = ret; } return *_length_result.at(_filter_keywords); } // returns the whole set of lines that would be seen with a long enough // screen, based on the current filtering mode. virtual void get_view_locked(set* lines) { if (_filter_keywords == G::FILTER_NONE) return; if (_filter_keywords == G::FILTER_OR) { for (auto & x : _keywords) { x->disjunctive_join(lines); } } else if (_filter_keywords == G::FILTER_AND) { bool first = true; for (auto & x : _keywords) { if (first) x->disjunctive_join(lines); else x->conjunctive_join(lines); first = false; } } } // sets the mode to no filter virtual inline void set_mode_none() { _filter_keywords = G::FILTER_NONE; } // sets the mode to AND filter virtual inline void set_mode_conjunction() { _filter_keywords = G::FILTER_AND; } // sets the mode to OR filter virtual inline void set_mode_disjunction() { _filter_keywords = G::FILTER_OR; } // returns true if there are no keywords virtual bool empty() const { return _keywords.size() == 0; } // returns true if there are more than one keyword virtual bool multiple() const { return _keywords.size() > 1; } // set of pinned positions during search PinManager _pins; // the current keyword that is being edited, to direct keystrokes to. If // it is nullptr than we are not currently editing a keyword. LineFilterKeyword *_current_keyword; // the list of all keywords we are searching for. the current keyword // above will be the last on this list vector> _keywords; // the keywords themselves as strings vector _keyword_vals; // current filtering mode int _filter_keywords = G::FILTER_NONE; // pointer to the log lines this filter manager is tied with LogLines* _ll; // thread safety mutable mutex _mutex; // navigation object attached to this runner Navigation* _navi; // first time we have two search terms, keep disjunction // afterwards, use conjuction if that is what the mode is bool _gentle_and; // length of the extra context around matching lines atomic _context_length; /* compute the number of matching lines, can take time so we implement * it async and display the result when it is finished */ future _display_length_future; // stores the computed length results until invalidated vector> _length_result; // signals that we need to recount the length because it has changed atomic _clear_results; // stored value from the future since getting it moves it out size_t _display_length; // renderer holds the render queue where we put our rendered lines Renderer* _renderer; // status bar line for the list of keywords optional _keyword_string; // true if the keyword string is volatile, such as an ongoing search bool _keyword_string_updating; }; #endif // __FILTER_MANAGER__H__ logserver/src/filter_renderer.h000066400000000000000000000542031512456611200172100ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __FILTER_RENDERER__H__ #define __FILTER_RENDERER__H__ #include #include #include #include #include "config.h" #include "constants.h" #include "format_string.h" #include "log_lines.h" #include "line_filter_keyword.h" #include "navigation.h" #include "pin_manager.h" #include "render_parms.h" #include "render_queue.h" #include "renderer.h" #include "search_range.h" using namespace std; /* Filter renderer is the engine in charge of computing what each display line * should look like. Each new render epoch kicks off a filter renderer's * run_render method. It takes a snapshot copy of the render parameters in * RenderParms (e.g., width, height, search words, etc.) and computes the lines * to render, passing them to the renderer. If the epoch advances during its * render, it exits. */ class FilterRenderer { public: /* Constructor takes the renderer and reference to the epoch */ FilterRenderer(Renderer* renderer, const Epoch& epoch) : _down_context_end(G::NO_POS), _up_context_end(G::NO_POS), _renderer(renderer), _epoch(epoch), _cur(G::NO_POS) { } // default destructor virtual ~FilterRenderer() {} // Kick off a rendering. The parameter parms stores all the relevant // parameters. Takes ownership of parms virtual void run_render(RenderParms* parms) { prepare_display(parms); _parms.reset(parms); // there is a single filter renderer, so we need to signal which // loglines we are currently rendering _navi = _parms->navi; _cur = _navi->cur(); // remember for up/down optimization _ll = _parms->ll; _max_digits = nullopt; reenter: // special case: we jumped into a loglines at a line, // but that line is not yet loaded. while (true) { // check if render is aborted if (_parms->cur_epoch != _epoch.cur()) return; // not an issue, we can ignore if (_navi->at_end()) break; if (_navi->cur() <= _ll->length()) break; // small wait this_thread::sleep_for(chrono::milliseconds(5)); } // step 1. draw everything that is available now. { // lock loglines auto lock = _ll->read_lock(); _parms->total_length = _ll->length(); fill_display(); draw_display(); } // unlock loglines // check if still useful to continue if (_parms->cur_epoch != _epoch.cur()) return; _navi->set_view(_display); // step 2. until the epoch advances, keep running to see if // searching has found more relevant lines we can render // set the endpoints until where we have successfully rendered // endpos points to the top and bottom lines in display we // havn't computed yet size_t endpos[2]; set_endpos(endpos); while (endpos[0] != G::NO_POS || endpos[1] != _display.size()) { // abort checks if (_parms->cur_epoch != _epoch.cur()) return; // allow some progress, then check if new render wanted bool progress = false; size_t curlines = _ll->length(); if (curlines != _parms->total_length) { _parms->total_length = curlines; // we're in tail mode, and new lines have come. // sleep a little and keep updating if (_navi->cur() == G::NO_POS) { this_thread::sleep_for(chrono::milliseconds(50)); goto reenter; } } auto lock = _ll->read_lock(); // if centre doesn't exist, draw it, otherwise try to // extend either in the directions. if (endpos[1] == _parms->radius) { if (_parms->cur_epoch != _epoch.cur()) return; if (set_centreline()) { draw_linenumber(_display[endpos[1]], endpos[1]); ++endpos[1]; progress = true; } // otherwise we have a centre, draw the first below line } else if (endpos[1] < _display.size()) { if (_parms->cur_epoch != _epoch.cur()) return; if (set_below_centreline(endpos[1] - _parms->radius)) { draw_linenumber(_display[endpos[1]], endpos[1]); ++endpos[1]; progress = true; } } // if we can do an aboveline as well do it if (endpos[0] != G::NO_POS) { if (_parms->cur_epoch != _epoch.cur()) return; if (set_above_centreline(_parms->radius - endpos[0])) { draw_linenumber(_display[endpos[0]], endpos[0]); --endpos[0]; progress = true; } } lock.unlock(); if (!progress) { _navi->set_view(_display); this_thread::sleep_for(chrono::milliseconds(50)); // abort checks happen at start of loop } } // we have now rendered all missing lines. set the // pageup/pagedown in the navi appropriate for the filtered // linue numbers if (_parms->cur_epoch != _epoch.cur()) return; _navi->set_view(_display); } protected: /* tries to reuse previous work when the only change seems to be going * up or down one line */ virtual void prepare_display(RenderParms* parms) { if (_parms.get()) [[likely]] { // if only one epoch advanced and it was caused by // moving vertically if (parms->cur_epoch == _parms->cur_epoch + 1 && _parms->navi->cur() != _cur && _cur != G::NO_POS && _parms->radius * 2 + 1 == _display.size() && _parms->navi->cur() != G::NO_POS) { slide_display(parms->navi->cur()); return; } } // default is all unset display since anything could have // changed _display = vector(2 * parms->radius + 1, G::NO_POS); } /* looks in the display for a line at to_pos and recentres display */ virtual void slide_display(size_t to_pos) { vector new_display(_display.size(), G::NO_POS); size_t index; for (index = 0; index < _display.size(); ++index) { if (_display[index] == to_pos) break; } // we didn't find it somehow if (index == _display.size()) [[unlikely]] { _display = new_display; return; } size_t to_index = _display.size() / 2; assert(to_index * 2 + 1 == _display.size()); size_t from_index = index; index = 0; while (true) { if (from_index + index >= _display.size()) break; if (to_index + index >= new_display.size()) break; new_display[to_index + index] = _display[from_index + index]; ++index; } index = 1; while (true) { if (index > from_index) break; if (index > to_index) break; new_display[to_index - index] = _display[from_index - index]; ++index; } _display = new_display; } // display is a list of numbers, which may have continuous G::NO_POS // from the top and bottom. This function sets endpos to the top and // bottom indices where this is the case, and to (G::NO_POS, // display.size()) otherwise, which are one past the ends virtual void set_endpos(size_t endpos[2]) const { // fast path, we have a full screen if (_display.front() != G::NO_POS && _display.back() != G::NO_POS) [[likely]] { endpos[0] = G::NO_POS; endpos[1] = _display.size(); return; } // slow path, compute boundary endpos[0] = _parms->radius - 1; endpos[1] = _parms->radius; while (endpos[0] != G::NO_POS && _display[endpos[0]] != G::NO_POS) { // we drew to top line if (_display[endpos[0]] == 0) endpos[0] = G::NO_POS; else --endpos[0]; } while (endpos[1] < _display.size() && _display[endpos[1]] != G::NO_POS) { ++endpos[1]; } } /* TODO: these functions are more than double the size just for handling * context. Should refactor to be simpler, and handle context lines * another way if possible. */ // computes the loglines position that should be rendered on the // centreline and sets it in display. Typically this is navi->cur(), // however if we have a filter applied and navi->cur() is not actually // rendered for that filter, then it is the next line after that would // be rendered. sets it to G::NO_POS if there is no line. virtual bool set_centreline() { _down_context_end = G::NO_POS; _up_context_end = G::NO_POS; if (!_parms->total_length) { _display[_parms->radius] = G::NO_POS; return false; } _display[_parms->radius] = _navi->cur(); if (_parms->filter_keywords == G::FILTER_NONE) { // pass } else if (_parms->filter_keywords == G::FILTER_OR || _parms->filter_keywords == G::FILTER_AND) { size_t search = _display[_parms->radius]; if (search == G::NO_POS) search = _parms->total_length; // slide to nearest _display[_parms->radius] = keyword_junction(search, G::DIR_DOWN); // e.g. the rest of this function only handles context // length. refactor so it is easier to deal with if (_parms->context_length) { size_t up = keyword_junction(search, G::DIR_UP); if (up != G::NO_POS && up + _parms->context_length > search) { _up_context_end = safeup(up, _parms->context_length); _down_context_end = safedown(up, _parms->context_length, _parms->total_length); } if (search + _parms->context_length >= _display[_parms->radius]) { if (_up_context_end == G::NO_POS) _up_context_end = safeup(_display[_parms->radius], _parms->context_length); _down_context_end = safedown(_display[_parms->radius], _parms->context_length, _parms->total_length); _display[_parms->radius] = search; } else if (up != G::NO_POS && up + _parms->context_length >= search) { _display[_parms->radius] = search; } else { assert(_parms->context_length < _display[_parms->radius]); _display[_parms->radius] = safeup(_display[_parms->radius], _parms->context_length); } } } return _display[_parms->radius] != G::NO_POS; } /* sets the value display[CENTER - pos + 1] to be the loglines position * that should be displayed on that position in the screen. Pos is set * to the number of lines above centerline we are displaying. For no * filter it is just the sequential line number until we reach zero. For * a filter we have to compute the next line that matches, unless we * havn't searched all the way for all our keywords in which case we * stop at the first gap and set it to G::NO_POS */ virtual bool set_above_centreline(size_t pos) { assert(pos >= 1); if (_parms->filter_keywords == G::FILTER_NONE) { if (pos == 1 && _navi->cur() == G::NO_POS && _parms->total_length) { _display[_parms->radius - pos] = _parms->total_length - 1; return true; } if (_display[_parms->radius - pos + 1] == G::NO_POS) return false; _display[_parms->radius - pos] = _display[_parms->radius - pos + 1] - 1; return true; } else if (_parms->filter_keywords == G::FILTER_OR || _parms->filter_keywords == G::FILTER_AND) { size_t last_pos = _display[_parms->radius - pos + 1]; if (_parms->context_length && _up_context_end != G::NO_POS && last_pos != G::NO_POS && last_pos > _up_context_end) { // we are within context. accept line _display[_parms->radius - pos] = last_pos - 1; if (last_pos - _up_context_end <= _parms->context_length) { // in second half of context // check for range extend size_t next = keyword_junction( last_pos, G::DIR_UP); if (next != G::NO_POS && (next > _up_context_end //within context || _up_context_end - next <= _parms->context_length)) { // will overlap _up_context_end = safeup( next, _parms->context_length); } } if (_up_context_end + 1 == last_pos) _up_context_end = G::NO_POS; // if last_pos is zero, this is actually -1 so // not a line return last_pos; } if (pos == 1) { // special cases size_t searchpos = _display[_parms->radius]; // either not found or at end if (searchpos == G::NO_POS) searchpos = _navi->cur(); // at end if (searchpos == G::NO_POS) searchpos = _parms->total_length; // if still -1, then zero lines _display[_parms->radius - pos] = keyword_junction(searchpos, G::DIR_UP); } else if (_display[_parms->radius - pos + 1] != G::NO_POS) { _display[_parms->radius - pos] = keyword_junction( _display[_parms->radius - pos + 1], G::DIR_UP); } if (_display[_parms->radius - pos] != G::NO_POS && _parms->context_length) { _up_context_end = safeup( _display[_parms->radius - pos], _parms->context_length); if (_display[_parms->radius - pos + 1] != G::NO_POS && _display[_parms->radius - pos + 1] <= _display[_parms->radius - pos] + _parms->context_length) { _display[_parms->radius - pos] = _display[_parms->radius - pos + 1] - 1; } else { _display[_parms->radius - pos] = safedown( _display[_parms->radius - pos], _parms->context_length, _parms->total_length); } } return _display[_parms->radius - pos] != G::NO_POS; } assert(0 && "filter is somehow not set to NONE/AND/OR"); return false; } /* Sets display[CENTRE + pos] to store the loglines postion that should * be displayed on that line in the screen. For no filter, it is just * the next line until the end is reached. For a filter applied it * searches for the next appropriate line and displays it. If the search * range has not each the end for all keywords, it sets it to G::NO_POS. */ virtual bool set_below_centreline(size_t pos) { assert(pos >= 1); if (_parms->filter_keywords == G::FILTER_NONE) { if (_display[pos + _parms->radius - 1] + 1 >= _parms->total_length || _display[pos + _parms->radius - 1] == G::NO_POS) return false; _display[pos + _parms->radius] = _display[pos + _parms->radius - 1] + 1; return true; } else if (_parms->filter_keywords == G::FILTER_OR || _parms->filter_keywords == G::FILTER_AND) { if (_display[_parms->radius + pos - 1] == G::NO_POS) return false; size_t last_pos = _display[_parms->radius + pos - 1]; if (_parms->context_length && _down_context_end != G::NO_POS && last_pos < _down_context_end) { _display[_parms->radius + pos] = last_pos + 1; if (_down_context_end - last_pos <= _parms->context_length) { // we're in second half of context // check if the next hit extends range size_t next = keyword_junction(last_pos + 1, G::DIR_DOWN); if (next < _down_context_end // next is within context || next - _down_context_end <= _parms->context_length) { // next's context will overlap _down_context_end = safedown(next, _parms->context_length, _parms->total_length); } } if (last_pos + 1 == _down_context_end) _down_context_end = G::NO_POS; return true; } size_t next = keyword_junction( _display[_parms->radius + pos - 1] + 1, G::DIR_DOWN); if (next == G::NO_POS) { // no further matches _display[_parms->radius + pos] = G::NO_POS; return false; } _down_context_end = safedown(next, _parms->context_length, _parms->total_length); if (_parms->context_length > next || next - _parms->context_length < last_pos + 1) { // context brings us to top _display[_parms->radius + pos] = last_pos + 1; } else { // take the first in the context _display[_parms->radius + pos] = next - _parms->context_length; } assert (_display[_parms->radius + pos] != G::NO_POS); return true; } assert(0); return false; } // returns the next line to display based on keywords, given that we are // at position pos and moving in direction dir. virtual size_t keyword_junction(size_t pos, int dir) { // if we're at the end already, return no pase if (dir == G::DIR_DOWN && pos == _parms->total_length) return G::NO_POS; if (_parms->filter_keywords == G::FILTER_AND) { return _parms->pins->pinsider( pos, dir, SearchRange::keyword_conjunction( pos, dir, _parms->keywords, _parms->total_length)); } assert(_parms->filter_keywords == G::FILTER_OR); return _parms->pins->pinsider(pos, dir, SearchRange::keyword_disjunction( pos, dir, _parms->keywords, _parms->total_length)); } /* fill display populates the initial display with the loglines * positions that should be rendered on the screen. If the value cannot * be computed sets it to G::NO_POS so we can display something now and * update it as the searching proceeds. */ virtual void fill_display() { set_centreline(); for (size_t i = 1; i <= _parms->radius; ++i) { if (!set_above_centreline(i)) break; } for (size_t i = 1; i <= _parms->radius; ++i) { if (!set_below_centreline(i)) break; } for (const auto& x : _display) { if (x != G::NO_POS) consider_digit_length(x); } } /* Draws the display to the screen. For each line in display, calls * draw_linenumber for it. */ virtual void draw_display() const { if (_parms->tab_data) { _parms->tab_data->maybe_evict(); for (size_t i = 0; i < _display.size(); ++i) { if (_display[i] == G::NO_POS || _display[i] >= _parms->total_length) continue; Line* line_object = _ll->get_line_object( _display[i]); line_object->measure_tabs(_parms->tab_key, _parms->tab_data); } } for (size_t i = 0; i < _display.size(); ++i) { if (!draw_linenumber(_display[i], i)) { return; } } } /* Takes a single line and produces a FormatString for the render queue * to accept. * number: line number in loglines, G::NO_POS for no line to show * pos: line number on the screen * centre: whether the line is the centre one for highlighting * returns true if the render queue accepted the line and false if it * rejected it (due to bad epoch) */ virtual bool draw_linenumber(size_t number, size_t pos) const { FormatString *fs = new FormatString(); string prefix; if (number != G::NO_POS && number < _parms->total_length) { if (!_parms->line_numbers_off) { stringstream ss; zero_pad(number, &ss); ss << (number + 1) << " "; prefix = ss.str(); } if (_parms->pins->is_pinned(number)) prefix += '*'; else prefix += ' '; fs->add(prefix, 0); if (prefix.length() > _parms->cols) [[unlikely]] { // unlikely: display too narrow to show data } else { size_t remain = _parms->cols - prefix.length(); Line* line_object = _ll->get_line_object_unlocked(number); size_t line_length = line_object->render( remain, pos == _parms->radius, _parms->tab_key, _navi->tab(), _parms->suppressed_tabs, _parms->tab_data, fs); if (pos == _parms->radius) { _navi->set_cur_length(prefix.length() + line_length); } } highlight_keywords(fs); } else { // display the empty line prefix = " ~"; fs->init(prefix); highlight_keywords(fs); } // highlight if we are the centre line and that centre line is // also the one navi is on if (pos == _parms->radius && number == _navi->cur()) { fs->highlight(); } return _renderer->render_queue()->add(fs, pos, _parms->cur_epoch); } // Applies formatting rules for the line, such as highlighting keywords virtual void highlight_keywords(FormatString* fs) const { int colour = 0; for (const auto& x : _parms->keywords) { fs->mark(Colour::keyword_number_to_colour(colour), (static_cast(*x))); ++colour; } if (_parms->colour) fs->colour_function(); } // Move up amount lines from whence safely. Returns 0 if amount is more // than whence. static size_t safeup(size_t whence, size_t amount) { if (whence == G::NO_POS) return whence; if (amount > whence) return 0; return whence - amount; } // Move down amount lines starting at whence safely. // retuns whence + amount if it is less than len, len - 1 if it is more, // and G::NO_POS if it is G::NO_POS static size_t safedown(size_t whence, size_t amount, size_t len) { if (whence == G::NO_POS) return whence; if (amount + whence >= len) return len - 1; return whence + amount; } /* gets the number of decimal digits in pos and sets max digits if it is * longer than it */ virtual void consider_digit_length(size_t pos) { // displayed number is 1-indexed but pos is 0-indexed ++pos; size_t len = 1; while (pos >= 10) { ++len; pos /= 10; } if (!_max_digits || *_max_digits < len) { _max_digits = len; } } // considers the line numbers that are being displayed and adds zeros to // small ones for alignment virtual void zero_pad(size_t number, stringstream* ss) const { // if we don't have a digit set or it is 1, then still pad the // number from 1-9 because they will have to shift as soon as // there is a 10th line. if (!_max_digits || *_max_digits < 2) { if (number + 1 < 10) *ss << '0'; return; } // add zeros based on the largest number we will display size_t cur = 10; for (size_t i = 1; i < *_max_digits; ++i) { // number is zero indexed but displayed as one indexed if (number + 1 < cur) *ss << '0'; cur *= 10; } } // viewport mapping lines on the display to the line numbers in // loglines. vector _display; // the largest number of digits we have on the display so far for // aligning the display optional _max_digits; // pointer to the log lines for getting line data LogLines* _ll; // navigation object attached to this runner Navigation* _navi; size_t _down_context_end; size_t _up_context_end; // renderer holds the render queue where we put our rendered lines Renderer* _renderer; // reference to global render epoch, to notice when we are rendering for // an expired epoch const Epoch& _epoch; // remember previous position of navigation for up/down optimization size_t _cur; // copy of relevant render parameters and pointers to key classes locked // in at the start of rendering. unique_ptr _parms; }; #endif // __FILTER_RENDERER__H__ logserver/src/format_string.h000066400000000000000000000144001512456611200167060ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __FORMAT_STRING__H__ #define __FORMAT_STRING__H__ #include #include #include "constants.h" using namespace std; /* FormatString extends string to include formatting instructions on it, such as * colour, bold, etc */ // TODO: infer more types of data that would benefit from colours class FormatString : public string { public: /* default constructor */ FormatString() : string("") {} // allow implicit conversion FormatString(const string& val) : string("") { init(val); } // destructor virtual ~FormatString() {} /* store the parameter value as the string, and creates a vector of * format rules of the same length */ virtual void init(const string& val) { assign(val); _fmt.resize(val.size()); } /* sets every character of the string to be bolded as highlighting */ virtual void highlight() { for (size_t i = 0; i < length(); ++i) { _fmt[i] |= G::BOLD; } } /* replace all occurances of char f with char r. used for making custom * chars into tabs */ virtual void replace(char f, char r) { for (size_t i = 0; i < length(); ++i) { if (at(i) == f) { (*this)[i] = r; } } } /* goes through the string, if it finds colons then put the thing before * it in COLON_COLOUR, when looking at key-value type pairs such as * json or configuration files. * TODO: maybe before = as well. */ virtual void colour_function() { // start at 1 since we colour backwards from a found ':' for (size_t i = 1; i < length(); ++i) { if (at(i) == ':') { // if followed by whitespace do the mark if (i + 1 < length() && at(i + 1) == ' ') { size_t j = i; while (j < length()) { if (white_at(j)) break; --j; } if (j >= length()) j = 0; mark(G::COLON_COLOUR, j, i + 1 - j); break; } size_t j; // scan ahead for more colons, only do the last for (j = i + 1; j < length(); ++j) { if (at(j) == ':') goto cont; } for (j = i; j < length(); --j) { if (white_at(j)) break; } if (j >= length()) j = 0; if (j + 1 < i) mark(G::COLON_COLOUR, j, i + 1 - j); } // for break-continue in nested loop cont:; } } /* depending on case sensitivity of keyword in the string, * make relevant findings of it in the line coloured with the code */ virtual void mark(int code, const string& keyword) { string lower = G::to_lower(keyword); if (lower != keyword) { mark(code, keyword, static_cast(*this)); } else { mark(code, lower, G::to_lower(static_cast(*this))); } } // TODO: avoid highlighting midstream matches for left/right anchors // wherever keyword appears, make it in the code colour virtual void mark(int code, const string& keyword, const string& line) { size_t start_pos = 0; while (npos != (start_pos = line.find(keyword, start_pos))) { mark(code, start_pos, keyword.length()); ++start_pos; } } /* returns the format for the position */ virtual int code(size_t pos) const { assert(pos < _fmt.size()); // high bit signals that format is from the line itself, so that // keyword matching gets priority over it return static_cast(_fmt.at(pos)) & 127; } /* used to increase length of string with spaces to size pos and adds * zeros to the format */ virtual void align(size_t pos) { while (pos > _fmt.size()) { append(" "); _fmt += '\0'; } assert(_fmt.size() == size()); } /* appends suffix and format to the string */ virtual void add(const string_view& suffix, const string_view& format) { assert(suffix.size() == format.size()); append(suffix); for (const auto& x : format) { _fmt += x ? static_cast(static_cast(x) | 128) : 0; } assert(_fmt.size() == size()); } /* appends suffix to the string with format in parameter code */ virtual void add(const string_view& suffix, int code) { assert(_fmt.size() == size()); append(suffix); for (size_t i = 0; i < suffix.length(); ++i) { _fmt += code ? static_cast(code | 128) : 0; } assert(_fmt.size() == size()); } /* appends a format string to this one */ virtual void add(const FormatString& other) { append(static_cast(other)); _fmt += other._fmt; assert(_fmt.size() == size()); } /* signals that the cursor is on this line at position in parameter */ virtual void cursor(size_t pos) { assert(pos < _fmt.size()); assert(_fmt[pos] == 0); _fmt[pos] = G::CURSOR_COLOUR; } virtual void truncate(size_t cols) { if (length() <= cols) return; size_t start = length() - cols; assert(start < length()); for (size_t i = 0; i < cols; ++i) { _fmt[i] = _fmt[i + start]; } init(substr(start)); } protected: /* returns true if position i is a whitespace character */ virtual bool white_at(size_t i) const { assert(i < length()); char c = at(i); return (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v'); } /* sets the string from pos to pos+len to have format code if it does * not have a format already. If highlighted then set it to code * highlighted. */ virtual void mark(int code, size_t pos, size_t len) { for (size_t i = pos; i < pos + len; ++i) { uint8_t val = static_cast(_fmt[i]); if (val == 0 // blank || val == G::BOLD // bold || val == G::UNDERLINE // underline || val > G::WEAK) { // input colour if (val > G::WEAK) val %= G::WEAK; val -= (val % G::LOWEST_MODIFIER); _fmt[i] = val + code; } } } /* same size as string, stores the format */ string _fmt; }; #endif // __FORMAT_STRING__H__ logserver/src/huge_vector.h000066400000000000000000000253111512456611200163450ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __HUGE_VECTOR__H__ #define __HUGE_VECTOR__H__ #include #include using namespace std; /* index in our vector of vectors, consisting of page and offset on that page */ struct pagepos_t { size_t page; size_t off; }; /* This class is for a huge vector, implemented as a vector of vectors. A deque * is inappropriate because we will rarely insert at the front, however it is * technically possible and we will occasionally insert at the middle to * implement line breaks. Allocating (and reallocating) huge vectors becomes * noticeable at very large sizes, so we have a template parameter max size and then * push a new vector for the next lines. Insertions can cause one vector to * grow, so we have to count elements from the start. */ // TODO: page size defaults to one million, which is the worst case for insertion // and reallocation. We should assess if a larger or smaller value is more // appropriate. /* huge_vector is templated on the type it stores T and the default size when a * new line is pushed on a new vector with PAGESIZE. Note that the default is * not a nice power of two to allow some number of insertions and line breaks * without risking a reallocation as a result */ template class huge_vector { public: class iterator { public: iterator(const huge_vector* ref) : _ref(ref) { _pp.page = 0; _pp.off = 0; } iterator(const huge_vector* ref, pagepos_t pp) : _ref(ref) { _pp = pp; } const T& operator*() const { return _ref->_lines[_pp.page][_pp.off]; } bool operator==(const iterator& other) { return !(*this != other); } bool operator!=(const iterator& other) { return _pp.off != other._pp.off || _pp.page != other._pp.page || _ref != other._ref; } iterator& operator++() { _ref->next(&_pp); return *this; } iterator& operator--() { _ref->prev(&_pp); return *this; } protected: pagepos_t _pp; const huge_vector* _ref; }; class const_iterator { public: const_iterator(const huge_vector* ref) : _ref(ref) { _pp.page = 0; _pp.off = 0; } const_iterator(const huge_vector* ref, pagepos_t pp) : _ref(ref) { _pp = pp; } const T& operator*() const { return _ref->_lines[_pp.page][_pp.off]; } bool operator==(const iterator& other) { return !(*this != other); } bool operator!=(const iterator& other) { return _pp.off != other.off || _pp.page != other.page || _ref != other._ref; } iterator& operator++() { _ref->next(_pp); return *this; } iterator& operator--() { _ref->prev(_pp); return *this; } protected: pagepos_t _pp; const huge_vector* _ref; }; /* constructor */ huge_vector() { clear(); } /* reset to empty */ virtual void clear() { _length = 0; _lines.clear(); _starts.clear(); } /* increment the pagepos to the next value, minding if we jump pages */ virtual void next(pagepos_t* pp) const { check(pp->page < _lines.size(), "next(): pp less than lines"); ++(pp->off); // we are at the end(). if (pp->page + 1 == _lines.size()) [[unlikely]] return; if (pp->off == _lines[pp->page].size()) [[unlikely]] { pp->off = 0; ++(pp->page); } } /* decrement the pagepos to the previous value, minding if we jump pages */ virtual void prev(pagepos_t* pp) const { check(pp->page < _lines.size(), "prev(): pp less than lines"); if (pp->off == 0) [[unlikely]] { if (pp->page) pp->off = _lines[pp->page - 1].size(); else pp->off = -1; --(pp->page); } --(pp->off); } /* returns true is the virtual size of this huge_vector is larger than * pos */ virtual bool valid(size_t pos) const { return valid(getpos(pos)); } /* checks if pagepos has a page and offset that exists */ virtual bool valid(pagepos_t pos) const { if (pos.page < _lines.size()) return pos.off < _lines.at(pos.page).size(); return false; } /* pushs a new item to the back of the huge_vector, minding if we need * to add a new page */ virtual void add(T&& item) { assert(_lines.size() == _starts.size()); if (_lines.empty() || _lines.back().size() >= PAGESIZE) [[unlikely]] { if (!_starts.empty()) [[likely]] { auto it = _starts.end(); --it; size_t newpos = it->first + _lines.back().size(); _starts[newpos] = _lines.size(); } else { _starts[0] = 0; } _lines.push_back(vector()); _lines.back().reserve(PAGESIZE); } assert(item != nullptr); _lines.back().push_back(std::move(item)); ++_length; } /* returns the number of elements */ virtual size_t length() const { return _length; } /* inserts a list of items at a position, faster than inserting * individially since we can slide all elements once the right amount. * returns the new length of the huge_vector */ virtual size_t insert(list& items, size_t pos) { if (items.size() == 0) return _length; pagepos_t pp = getpos(pos); check(valid(pp), "insert() pp not valid"); slide(pp, items.size()); auto it = items.begin(); auto& vec = _lines.at(pp.page); while (it != items.end()) { assert(it->get()); vec[pp.off] = std::move(*it); ++pp.off; ++it; } _length += items.size(); adjust_starts(); return _length; } /* inserts one item at a position */ virtual size_t insert(T&& item, size_t pos) { pagepos_t pp = getpos(pos); check(valid(pp), "insert() one, pp not valid"); _lines.at(pp.page).insert(_lines.at(pp.page).begin() + pp.off, std::move(item)); ++_length; adjust_starts(); return _length; } /* support array index operator overload for elegance */ T& operator[](size_t pos) { pagepos_t pp = getpos(pos); return _lines[pp.page][pp.off]; } /* const element access */ virtual const T& at(size_t pos) const { pagepos_t pp = getpos(pos); return _lines[pp.page][pp.off]; } /* removes the element at a position and updates length */ virtual void remove(size_t pos) { pagepos_t pp = getpos(pos); check(valid(pp), "remove() pp not valid"); _lines[pp.page].erase(_lines[pp.page].begin() + pp.off); --_length; adjust_starts(); } /* streams the entire sequence of lines to os */ virtual void write(ostream& os) const { for (const auto& y: _lines) { for (const auto& x : y) { os << x->get() << endl; } } } /* gets the string_view representation of the element at pos. This makes * an assumptions on T that it is a pointer to something that has a * view() function that returns a string_view, which is fine for * logserver */ virtual const string_view view_at(size_t pos) const { pagepos_t pp = getpos(pos); return _lines[pp.page][pp.off]->view(); } /* Helper function to make matching faster. Iterates over pagepos_t * directly and runs matching callback fn() adding the position to the * results set if fn returns true. This avoid having to convert a * position to pagepos each line. */ virtual size_t range_add_if( size_t start, size_t end, set* results, const function& fn) const { assert(results); if (end < start) { size_t swap = end; end = start; start = swap; } if (start >= _length) return _length; if (end > _length) end = _length; pagepos_t pp = getpos(start); for (size_t i = start; i < end; ++i) { if (fn(_lines[pp.page][pp.off]->view())) { results->insert(results->end(), i); } next(&pp); } return _length; } /* consistency checker used for unit tests */ virtual void sanity() { assert(_lines.size() == _starts.size()); size_t pos = 0; auto it = _starts.begin(); for (size_t i = 0; i < _lines.size(); ++i) { assert(it->first == pos); assert(it->second == i); pos += _lines[i].size(); ++it; } } virtual iterator begin() const { return iterator(this); } virtual const_iterator cbegin() const { return const_iterator(this); } virtual iterator end() const { if (!_lines.size()) return iterator(this); pagepos_t pp; pp.page = _lines.size() - 1; pp.off = _lines[_lines.size() - 1].size(); return iterator(this, pp); } virtual const_iterator cend() const { if (!_lines.size()) return const_iterator(this); pagepos_t pp; pp.page = _lines.size() - 1; pp.off = _lines[_lines.size() - 1].size(); return const_iterator(this, pp); } protected: // we have inserted or deleted somewhere and need to change all start // keys after that point. since this is infrequent and the shuffling is // more costly just recompute it. virtual inline void adjust_starts() { _starts.clear(); int pos = 0; for (size_t i = 0; i < _lines.size(); ++i) { _starts[pos] = i; pos += _lines[i].size(); } } /* converts a virtual huge_vector index into a page/pos combination */ virtual inline pagepos_t getpos(size_t pos) const { pagepos_t ret; // optimize when we don't have a huge vector, such as only a // single page, or the pos is on that first page. if (_lines.empty() || pos < _lines.front().size()) [[likely]] { ret.page = 0; ret.off = pos; return ret; } // find the relevant page and compute its offset on it auto it = _starts.upper_bound(pos); assert(it != _starts.begin()); --it; ret.page = it->second; assert(it->first <= pos); ret.off = pos - it->first; return ret; } /* implements the slide needed to insert new elements. pp is the * page/pos to insert at, and amount is the number of new elements to * support */ virtual void slide(pagepos_t& pp, size_t amount) { if (!amount) return; auto& vec = _lines.at(pp.page); size_t pos = vec.size() - 1; vec.resize(vec.size() + amount); while (true) { assert(vec[pos].get()); vec[pos + amount] = std::move(vec[pos]); assert(vec[pos].get() == nullptr); if (pos == pp.off) break; --pos; } } // used in assertions to optimize out in release. checks valid page pos static inline bool check(bool val, const string& text) { if (!val) [[unlikely]] { throw runtime_error(text); } return true; } // the vector of vectors vector> _lines; // the length of the vector size_t _length; // tracks first element global offset per page map _starts; }; #endif // __HUGE_VECTOR__H__ logserver/src/i_line_provider.h000066400000000000000000000021541512456611200172040ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __I_LINE_PROVIDER__H__ #define __I_LINE_PROVIDER__H__ #include using namespace std; /* Line providers are sources for data into logserver. This class providers the * interface and is implemented by file readers, pipe readers, etc. */ class ILineProvider { public: virtual void start() = 0; virtual string get_line(size_t pos) = 0; virtual ~ILineProvider() {} }; #endif // __I_LINE_PROVIDER__H__ logserver/src/i_log_lines.h000066400000000000000000000027331512456611200163210ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __I_LOG_LINES__H__ #define __I_LOG_LINES__H__ #include using namespace std; class Line; // interface for line providers to not need to include loglines.h class ILogLines { public: // allow subclasses virtual ~ILogLines() {} // adds a new line to the log lines and returns its pos virtual size_t add_line(const string& line) = 0; // adds a Line object to log lines and returns its pos virtual size_t add_line(Line* line) = 0; // batch adds Line objects to log lines and removes them from the passed // in list virtual void add_lines(list* line) = 0; // sets eof if input reader is done virtual bool eof() const = 0; // unset eof, e.g., if the file being tailed is updated virtual void set_eof(bool value) = 0; }; #endif // __I_LOG_LINES__H__ logserver/src/i_log_lines_events.h000066400000000000000000000026341512456611200177050ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __I_LOG_LINES_EVENTS__H__ #define __I_LOG_LINES_EVENTS__H__ class Line; class LogLines; // interface for loglines's event for relevant listeners class ILogLinesEvents { public: // allow subclasses virtual ~ILogLinesEvents() {} // an existing line was changed virtual void edit_line(LogLines* ll, size_t pos, Line* line) = 0; // a new line was added virtual void append_line(LogLines* ll, size_t pos, Line* line) = 0; // an insertion event virtual void insertion(LogLines* ll, size_t pos, size_t amount) = 0; // a deletion event virtual void deletion(LogLines* ll, size_t pos, size_t amount) = 0; // clear all lines virtual void clear_lines(LogLines* ll) = 0; }; #endif // __I_LOG_LINES_EVENT__H__ logserver/src/interface.h000066400000000000000000000421661512456611200160020ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __INTERFACE__H__ #define __INTERFACE__H__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "constants.h" #include "curses_wrapper.h" #include "epoch.h" #include "key_groups.h" #include "log_lines.h" #include "match.h" #include "navigation.h" #include "renderer.h" #include "run.h" #include "story.h" #include "typing_line.h" #include "version.h" #include "view_stack.h" using namespace std; using namespace std::placeholders; /* Operates the user interface to logserver, calling getch() and processing the * event. It holds a ViewStack instance, which holds the data structure where * all non-interface events are processed, and holds the renderer and * epoch instances. */ class Interface { public: // constructed with the initial LogLines instances explicit Interface(LogLines* ll) : _state(0), _type(nullptr), _no_d(true), _type_offset(0), _mem_timer(0) { unique_lock ul(_m); _state = COMMAND; if (freopen("/dev/tty", "rw", stdin) == nullptr) { throw G::errno_string("cannot reopen /dev/tty", errno); } _renderer.reset(new Renderer( bind(&ViewStack::title_bar, &_views, _1, _2), bind(&Interface::status_bar, this, _1, _2), &_epoch)); _views.init(_renderer.get(), &_epoch); _views.push(ll); _renderer->start(); } /* signal we are exiting in case we havn't yet, and advance the epoch to * trigger any waiting threads */ virtual ~Interface() { ExitSignal::check(true); _epoch.shutdown(); } /* fuzzes the interface for testing purposes */ virtual void fuzz(size_t millis) { srand(time(NULL)); _views.check(); _epoch.advance(); int ch = 0; while (!ExitSignal::check(false)) [[likely]] { // get an appropriate character to enter if (_type.get()) ch = KeyGroups::random_type(); else ch = KeyGroups::random_action(); process(ch); _epoch.advance(); this_thread::sleep_for(chrono::milliseconds(millis)); } } /* man looping thread. gets a char, processes it, and checks if we are * exiting */ virtual void run() { _views.check(); _epoch.advance(); int ch = 0; while (!ExitSignal::check(false)) [[likely]] { ch = CursesWrapper::getch_blocking(); process(ch); /* when pasting in data keep reading characters and * sending without doing a re-render. * if we see a newline (enter) ignore it and everything * after, so user has to deliberately hit enter on a * pipe command */ bool keepsending = true; while (true) { ch = CursesWrapper::getch_nonblocking(); if (ch == ERR) break; if (ch == '\n') keepsending = false; try { if (keepsending) process(ch); } catch (const logic_error& le) { } catch (const runtime_error& re) { // TODO: this have are due to running // commands on an invalid line. add // info to a debug log to fix } } _epoch.advance(); } } protected: /* callback function for the renderer to draw the status bar */ virtual bool status_bar(FormatString* fs, size_t cols) { unique_lock ul(_m); stringstream ss; size_t highlight = 0; if (_state == COMMAND) { size_t len = _views.fm()->length(); ss << "[COMMAND] " << _views.fm()->mode_string() << " "; if (len != G::NO_POS) ss << len << " lines"; else ss << " "; if (_views.ll()->eof()) ss << " (eof)."; else ss << "."; ss << " "; for (size_t i = 0; i < _views.fm()->total_context(); ++i) { if (i == 10) break; ss << '+'; } ss << " keywords: "; } else if (_state == TYPE_LINENO) { ss << " :"; highlight = ss.str().length(); ss << _type->typed(); } else if (_state == TYPE_PIPE) { ss << " | "; highlight = ss.str().length(); ss << _type->typed(); } else if (_state == TYPE_MATCH) { ss << "> "; highlight = ss.str().length(); if (_type_offset) ss << "!"; ss << _views.fm()->current_keyword(); } else if (_state == TYPE_COMMENT) { ss << "[enter comment] "; highlight = ss.str().length(); ss << _type->typed(); } else if (_state == TYPE_SAVE || _state == TYPE_SAVELINE) { ss << "[filename] "; highlight = ss.str().length(); ss << _type->typed(); } else if (_state == TYPE_EDIT) { ss << "[edit " << _views.navi()->cur() << "] "; highlight = ss.str().length(); ss << _type->typed(); } else if (_state == TAB_KEY) { ss << "[tab for char] "; highlight = ss.str().length(); } else if (_state == BREAK_KEY) { ss << "[break line on char] "; highlight = ss.str().length(); } ss << " "; fs->add(ss.str(), 0); if (typing_state()) { if (highlight && _type.get()) { assert(highlight <= ss.str().length()); highlight += _type->get_pos() + _type_offset; fs->cursor(highlight); } else { highlight = 0; } } fs->add(_views.fm()->keyword_string()); string usage = mem_usage(); size_t offset = cols - 5 - usage.length(); fs->align(offset); fs->add(" ", 7); fs->add(usage, 7); return true; } /* returns a string for the memory usage */ virtual string mem_usage() { #ifdef __APPLE__ return ""; #endif // don't poll every render if (!_mem_timer) { getrusage(RUSAGE_SELF, &_usage); _mem_timer = G::USAGE_TIMER; } --_mem_timer; stringstream ss; ss << (_usage.ru_maxrss / 1024) << " MiB"; return ss.str(); } /* process a keystroke. A static map of functions handles all the ones * that we can call directly without arguments in this class or * ViewStack. Similarly, the navigation instance has a function that * checks if it is a keystroke it handles, like arrow directions. * Otherwise a switch statement handles the other events */ virtual void process(int ch) { static map> functions; unique_lock ul(_m); if (functions.empty()) [[unlikely]] { // functions involving interface functions['|'] = bind(&Interface::start_pipe, this); functions[':'] = bind(&Interface::start_lineno, this); functions['/'] = bind(&Interface::start_match, this, Match::PRESENT); functions['#'] = bind(&Interface::start_comment, this); functions['e'] = bind(&Interface::start_edit, this); functions['S'] = bind(&Interface::start_save, this); functions['s'] = bind(&Interface::start_saveline, this); functions['^'] = bind(&Interface::start_match, this, Match::PRESENT | Match::ANCHOR_LEFT); functions['$'] = bind(&Interface::start_match, this, Match::PRESENT | Match::ANCHOR_RIGHT); functions['\\'] = bind(&Interface::start_match, this, Match::MISSING); functions['i'] = bind(&Interface::info, this, false); // functions involving view stack functions['f'] = bind(&ViewStack::follow, &_views, false); functions['F'] = bind(&ViewStack::follow, &_views, true); functions['!'] = bind(&ViewStack::insert_dash_line, &_views); functions['\b'] = functions[127] = functions[KEY_BACKSPACE] = bind(&ViewStack::backspace, &_views); functions['b'] = bind(&ViewStack::break_line, &_views); functions[27] = bind(&ViewStack::pop, &_views); functions['h'] = bind(&ViewStack::help, &_views); functions['%'] = bind(&ViewStack::permafilter, &_views); functions['\n'] = bind(&ViewStack::enter, &_views); functions['C'] = bind(&ViewStack::clear, &_views); functions['<'] = bind(&ViewStack::lshift, &_views); functions['>'] = bind(&ViewStack::rshift, &_views); functions['*'] = bind(&ViewStack::pin_line, &_views); functions['m'] = bind(&ViewStack::merge, &_views); functions['t'] = bind(&ViewStack::tab_toggle, &_views); functions['1'] = bind(&ViewStack::tab_suppress, &_views, 0); functions['2'] = bind(&ViewStack::tab_suppress, &_views, 1); functions['3'] = bind(&ViewStack::tab_suppress, &_views, 2); functions['4'] = bind(&ViewStack::tab_suppress, &_views, 3); functions['5'] = bind(&ViewStack::tab_suppress, &_views, 4); functions['6'] = bind(&ViewStack::tab_suppress, &_views, 5); functions['7'] = bind(&ViewStack::tab_suppress, &_views, 6); functions['8'] = bind(&ViewStack::tab_suppress, &_views, 7); functions['9'] = bind(&ViewStack::tab_suppress, &_views, 8); functions['0'] = bind(&ViewStack::tab_suppress, &_views, 9); } // handle interface state // reset info frustration if (ch != 'i' && ch != 'I') _frustration = -1; if (ch != 'd') _no_d = true; // Check if we are in a different state that command mode, where // keystrokes are, e.g., typing a search string. if (_state == TYPE_MATCH) { assert(_type.get()); if (_type->process(ch)) { finish_type(); _type_offset = 0; } else { _views.fm()->current_type(_type->typed()); } } else if (typing_state()) { assert(_type.get()); if (_type->process(ch)) finish_type(); } else if (waitchar_state()) { waitchar_finish(ch); } else if (_state == COMMAND) { /* These [[unlikely]] tags aren't necessarily true, but * if they are true, then we go back to waiting on * input, so we might as well pessimistically assume our * work will continue. */ // If command requires a line but we aren't on one skip if (KeyGroups::requires_line(ch) && (_views.navi()->at_end() || _views.ll()->length() == 0)) [[unlikely]] return; // We are in command mode, process the keystroke // appropriately. First see if navigation handles it if (_views.navi()->process(ch)) [[unlikely]] return; // Second see if it is in our list of functions if (functions.count(ch)) [[unlikely]] { functions.at(ch)(); return; } // handle remaining characters switch (ch) { case 'q': ExitSignal::check(true); _epoch.shutdown(); break; case 337: _views.navi()->goto_line( _views.fm()->keyword_slide( G::DIR_UP), false); break; case 336: _views.navi()->goto_line( _views.fm()->keyword_slide( G::DIR_DOWN), false); break; case KEY_SRIGHT: _views.navi()->goto_pos( _views.fm()->find_next_match()); break; case KEY_SLEFT: _views.navi()->goto_pos( _views.fm()->find_prev_match()); break; case '\t': _views.fm()->toggle_mode(); break; case '+': _views.fm()->add_context(); break; case '-': _views.fm()->remove_context(); break; case 'n': _renderer->line_numbers_toggle(); break; case 'c': _renderer->colour_toggle(); break; case 'B': _state = BREAK_KEY; break; case 'T': _state = TAB_KEY; break; case 'd': if (_no_d) _no_d = false; else if (!_views.navi()->at_end()) { _views.ll()->remove(_views.navi()->cur()); _views.navi()->up(); _no_d = true; } break; } } } /* user hit 'i' or 'I' for line intelligence. Handle it and track * frustation */ virtual void info(bool full) { if (_views.ll()->length() == 0) return; _views.ll()->info(_views.navi()->cur(), full, ++_frustration); } /* User hit either enter or escape, to accept or reject the current * typing line state. Handle the result appropriately based on what * started the typing line. */ virtual void finish_type() { bool accepted = _type->result(); const string& typed = _type->typed(); if (_state == TYPE_MATCH) { // finish a keyword search _views.finish_match(accepted); } else if (_state == TYPE_PIPE) { // run a pipe command and push a new loglines if (accepted && !Tokenizer::trim(typed, " |\t\n").empty()) { _views.run_pipe_command(typed); } } else if (_state == TYPE_LINENO) { // just to a line number if (accepted) { try { size_t lineno = stoi(typed.c_str()); if (lineno) --lineno; if (lineno > _views.ll()->length()) lineno = _views.ll()->length() - 1; _views.navi()->goto_line(lineno, true); } catch (const logic_error& lg) {} } } else if (_state == TYPE_COMMENT) { // add a comment to the story log if (accepted) { set view; if (!_views.fm()->is_mode_all()) { _views.fm()->get_view(&view); } _story.write(typed, _views.ll(), _views.navi(), view); } } else if (_state == TYPE_SAVE) { // save filename if (accepted) { _views.ll()->save(typed); } } else if (_state == TYPE_SAVELINE) { // save filename if (accepted) { _views.ll()->save_line(typed, _views.navi()->cur()); } } else if (_state == TYPE_EDIT) { // edit the line, replace it in loglines if (accepted) { _views.ll()->set_line_unlocked(_views.navi()->cur(), typed); } } _type.reset(nullptr); _state = COMMAND; } /* start a save typing line */ virtual void start_save() { _state = TYPE_SAVE; _type.reset(new TypingLine()); } /* start a editline typing line */ virtual void start_edit() { assert(_views.ll()->length() != 0); assert(!_views.navi()->at_end()); _state = TYPE_EDIT; _type.reset(new TypingLine()); _type->set_type(_views.ll()->get_line_unlocked(_views.navi()->cur())); } /* start a saveline typing line */ virtual void start_saveline() { assert(_views.ll()->length() != 0); assert(!_views.navi()->at_end()); _state = TYPE_SAVELINE; _type.reset(new TypingLine()); } /* start a comment typing line */ virtual void start_comment() { assert(_views.ll()->length() != 0); assert(!_views.navi()->at_end()); _state = TYPE_COMMENT; _type.reset(new TypingLine()); } /* start a pipe command typing line */ virtual void start_pipe() { _state = TYPE_PIPE; _type.reset(new TypingLine()); } /* start a line number jump typing line */ virtual void start_lineno() { _state = TYPE_LINENO; _type.reset(new TypingLine()); _type->only_numbers(); } /* Returns true if we are currently in a typing line state where * keystrokes are meant to edit the typing line */ virtual bool typing_state() const { return _state == TYPE_COMMENT || _state == TYPE_MATCH || _state == TYPE_PIPE || _state == TYPE_LINENO || _state == TYPE_SAVE || _state == TYPE_SAVELINE || _state == TYPE_EDIT; } /* Returns true if we are waiting on a single character to complete the * event, such as break-with-particular-char. */ virtual bool waitchar_state() const { return _state == BREAK_KEY || _state == TAB_KEY; } /* Handle arrival of a char in the waitchar state */ virtual void waitchar_finish(char c) { if (_state == BREAK_KEY) { _views.ll()->split(_views.navi()->cur(), c); } else if (_state == TAB_KEY) { _views.tab_key(c); } else { assert(0); } _state = COMMAND; } /* Begin a new string match event, with the anchor and reverse flags * based on how it was launched */ virtual void start_match(int match_criteria) { _state = TYPE_MATCH; // TODO simplify this. if true it adds an extra space to account // for the bang before search time but logic shouldn't be so // coupled here _type_offset = match_criteria & Match::MISSING; _views.fm()->start_match(match_criteria, _views.navi()->cur()); _type.reset(new TypingLine()); } // current state of the interface controls how keystrokes are processed int _state; /* states for logserver interface */ static constexpr int COMMAND = 0; static constexpr int TYPE_MATCH = 1; static constexpr int TYPE_COMMENT = 2; static constexpr int TYPE_PIPE = 3; static constexpr int TYPE_LINENO = 4; static constexpr int TYPE_SAVE = 5; static constexpr int TYPE_SAVELINE = 6; static constexpr int BREAK_KEY = 7; static constexpr int TYPE_EDIT = 8; static constexpr int TAB_KEY = 9; /* end states */ // main epoch class, last to be destructed since worker threads will // check the epoch during exit checks Epoch _epoch; // renderer object, behaviour can be changed by interface unique_ptr _renderer; // holds all the loglines, filterers, and navigation ViewStack _views; // if nullptr, we are not in a typing state, otherwise holds the // instance of what the typing line is unique_ptr _type; // for thread safety when the renderer invokes the status and title bar // callbacks mutex _m; // memory usage, reuses old result and updates periodically struct rusage _usage; // how many times the user has pressed 'i' to relax heuristics int _frustration; // whether last char was a 'd', for the 'dd' delete line command bool _no_d; // used when adding comments based on exploration Story _story; // stores offset for a typing line that is beyond default of 2 int _type_offset; // couter to check memory usage int _mem_timer; }; #endif // __INTERFACE__H__ logserver/src/key_groups.h000066400000000000000000000036401512456611200162230ustar00rootroot00000000000000#ifndef __KEY_GROUPS__H__ #define __KEY_GROUPS__H__ #include #include #include using namespace std; /* Groups different keystokes. Used in fuzz testing to generate random * appropriate keys strokes. Can be used to test if a keycode has a particular * property */ class KeyGroups { public: /* random key in the typing line state */ static int random_type() { static vector _choices = { '\n', '\n','\n', 'a', 'b', 'c', 'd', 'e', 'f', 'g', '0', '1', '2', '3', '4', '5', '6', }; if (rand() % 2) return random_direction(); return _choices[rand() % _choices.size()]; } /* random key in the command mode state */ static int random_action() { static vector _choices = { ':', '/', '#', 'e', '^', '$', '\\', '\n', 'i', 'f', '!', '\b', 'b', 27, '%', 'C', '<', 't', 'm', 'T', 'c', '>', '*', 'p', 'n', 'B', 'd', '-', '+', 551, 556, 336, 337, 'q', KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_PPAGE, KEY_NPAGE, KEY_HOME, KEY_END, KEY_SHOME, KEY_SEND}; return _choices[rand() % _choices.size()]; } /* random direction keystroke */ static int random_direction() { static vector _choices = { 336, 337, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_PPAGE, KEY_NPAGE, KEY_HOME, KEY_END, KEY_SHOME, KEY_SEND}; return _choices[rand() % _choices.size()]; } /* set of commands that work on a line, to skip if navigation is at end * or lines are empty */ static bool requires_line(int ch) { static set _keys = { '#', 'e', 's', 'i', 'f', 'F', 'y', 'b', '\n', '*', 'p', 'd', 'B'}; return _keys.count(ch); } }; #endif // __KEY_GROUPS__H__ logserver/src/line.h000066400000000000000000000176131512456611200147700ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __LINE__H__ #define __LINE__H__ #include #include #include #include #include "format_string.h" #include "tab_data.h" using namespace std; #ifdef __USE_GEB__ #include "graph.h" #include "map_strings.h" using TGraph = Graph; #endif #include /* stores a single line in the loglines. while normally a string, this allows * storing extra metadata per line. For example, if it was modified, what was * the original line so we can revert it back. As well, for GEB integration is * tracks a node in the graph tied to the line */ class Line { public: // constructors take a string and a possible GEB node explicit Line(const string_view& line) : _line(line) {} explicit Line(const string& line) : _line(line) {} Line(const string& line, const string& fmt) : _line(line), _fmt(fmt) {} #ifdef __USE_GEB__ Line(const string_view& line, TGraph::Node* node) : _line(line), _node(node) {} Line(const string& line, TGraph::Node* node) : _line(line), _node(node) {} #endif virtual ~Line() {} virtual void operator+=(const Line& other) { /* either string can have empty string for format, as a * shorthand for all zeros to avoid duplicating storage for long * string when formatting is not being provided by the input * stream. handle these cases by degraded to stored */ if (_fmt.empty() && other._fmt.empty()) { // both unset format } else if (_fmt.empty()) { _fmt = string(length(), '\0') + other._fmt; } else if (other._fmt.empty()) { _fmt += string(other.length(), '\0'); } else { _fmt += other._fmt; } _line += other._line; _no_tabs = nullopt; } // length of the string virtual inline size_t length() const { return _line.length(); } // string_view for the string virtual inline string_view view() const { return _line; } virtual inline string_view format_view() const { return _fmt; } // the string to show virtual inline const string& get() const { return _line; } // return to the original value virtual inline void revert() { if (_original.get() == nullptr) return; _line = std::move(*_original.get()); _no_tabs = nullopt; _fmt = ""; _original.reset(); } // if we do not have an original value saved, save it now. // TODO: we could have a stack to support popping multiple times virtual inline void maybe_save() { if (_original.get() == nullptr) { _original.reset(new string(std::move(_line))); } } // set the string to a new value, save the old if needed virtual inline void set(const string& val) { maybe_save(); _line = val; _fmt = ""; _no_tabs = nullopt; } // change the string at character col to have value val virtual inline void mark(size_t col, char val) { maybe_save(); _line[col] = val; } virtual void set_format(string&& format) { _fmt = format; } virtual list tabstops(optional tab_key) { assert(tab_key); list ret; bool quote = false; bool escape = false; size_t i = 0; ret.push_back(0); for (const auto &x : _line) { if (x == *tab_key && !quote) { ret.push_back(i); escape = false; } else if (x == '\\') { escape = !escape; } else if (x == '\"' && !escape) { escape = false; quote = !quote; } else { escape = false; } ++i; } return ret; } virtual Line* filter_tabs(optional tab_key, const std::set& suppress_cols) { string result = ""; list stops = tabstops(tab_key); auto it = stops.begin(); auto it_next = it; ++it_next; size_t cur_tab_index = 0; bool first = true; while (it_next != stops.end()) { if (!suppress_cols.count(cur_tab_index)) { if (!first) result += *tab_key; first = false; if (*it == 0) { result += _line.substr(0, *it_next); } else { result += _line.substr(*it + 1, *it_next - *it - 1); } } ++it_next; ++it; ++cur_tab_index; } return new Line(string(result)); } virtual void measure_tabs(optional tab_key, TabData* tab_data) { assert(tab_key); list stops = tabstops(tab_key); auto it = stops.begin(); auto it_next = it; ++it_next; size_t cur_tab_index = 0; while (it_next != stops.end()) { tab_data->observe_col(cur_tab_index, *it_next - *it + 1); ++it; ++it_next; ++cur_tab_index; } } virtual size_t render(size_t remain, bool need_length, optional tab_key, size_t navi_tab, const std::set& suppressed_tabs, TabData* tab_data, FormatString* fs) { check_notabs(); if (tab_key == nullopt && _no_tabs == true) [[likely]] { // too far right (or empty line) if (navi_tab > _line.size()) return _line.size(); // add remaining string if (_fmt.empty()) { fs->add(_line.substr(navi_tab, remain), 0); } else { fs->add(_line.substr(navi_tab, remain), _fmt.substr(navi_tab, remain)); } return _line.size(); } // slow path. we have to worry about tabs. string detabbed; string detabbed_format; detabbed.reserve(2 * _line.size()); detabbed_format.reserve(2 * _line.size()); size_t tab_index = 0; // just simplifies logic by normalizing optional to tab if (tab_key == nullopt) tab_key = '\t'; list stops = tabstops(tab_key); bool omit = suppressed_tabs.count(tab_index); size_t tab_start = 0; auto it = stops.begin(); ++it; for (size_t i = 0; i < _line.size(); ++i) { char x = _line[i]; if (it != stops.end() && i == *it) { ++it; if (!omit) { detabbed += ' '; detabbed_format += '\0'; if (tab_data) { size_t align = tab_data->width(tab_index); while (tab_start + align > detabbed.size()) { detabbed += ' '; detabbed_format += '\0'; } tab_start = detabbed.size(); } else { while (detabbed.size() % 8) { detabbed += ' '; detabbed_format += '\0'; } } } if (tab_data) { ++tab_index; omit = suppressed_tabs.count(tab_index); } } else if (!omit) [[likely]] { detabbed += x; if (_fmt.empty()) detabbed_format += '\0'; else detabbed_format += _fmt[i]; } assert(detabbed.size() == detabbed_format.size()); // if the caller needs the length to support navigation // jumping to the end of line then compute the whole // length. otherwise we can stop here since we've got // the view of the string if (!need_length && detabbed.size() >= navi_tab + remain) [[unlikely]] break; } if (detabbed.size() > navi_tab) fs->add(detabbed.substr(navi_tab, remain), detabbed_format.substr(navi_tab, remain)); return detabbed.size(); } #ifdef __USE_GEB__ // the GEB node virtual inline TGraph::Node* node() const { return _node; } #endif protected: virtual inline void check_notabs() { if (_no_tabs != nullopt) return; _no_tabs = _line.find('\t') == string::npos; } // TODO: store the line, bitset for original, isall lower, // then string for original and lower. or craft lower matching alg // the string we will see string _line; // pointer to original string if relevant unique_ptr _original; string _fmt; optional _no_tabs; #ifdef __USE_GEB__ // GEB node for GEB integration TGraph::Node* _node; #endif }; #endif // __LINE__H__ logserver/src/line_filter_keyword.h000066400000000000000000000303031512456611200200700ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __LINE_FILTER_KEYWORD__H__ #define __LINE_FILTER_KEYWORD__H__ #include #include #include "config.h" #include "constants.h" #include "explored_range.h" #include "i_log_lines_events.h" #include "log_lines.h" #include "match.h" #include "navigation.h" using namespace std; using namespace std::placeholders; /* This class stores the notion of searching for a keyword in the log file. As * characters of the keyword are typed, that prefix is then sought so it is * responsive. Once the keyword is confirmed, it is set and all prefix searches * are removed */ class LineFilterKeyword : public ILogLinesEvents { public: /* Constructor takes logline looking up lines and registering for * updates, along with search parameters */ LineFilterKeyword(const LogLines& log_lines, int match_parms, size_t whence) : _ll(log_lines), _match(match_parms), _state_changed(false) { set_whence(whence); EventBus::_()->enregister(&_ll, this); _exit = false; _worker.reset(new thread(&LineFilterKeyword::searcher, this)); } /* unregister for updates if we are present, and join the worker thread */ virtual ~LineFilterKeyword() { // remove first to avoid lock contention EventBus::_()->deregister(&_ll, this); unique_lock ul(_m); _state_changed = true; _exit = true; ul.unlock(); _worker->join(); } virtual void edit_line([[maybe_unused]] LogLines* ll, size_t pos, Line* line) override { unique_lock ul(_m); if (!_finished) return; assert(ll == &_ll); if (_match.is_match(line->view())) { _partial_range.front()->add_finding(pos); } else { // in the very unlikely event where search already looked // at old version of a line and found a match, but // hasn't reported the results during which time we've // edited it to remove the match and are about to try to // remove it (but it is not there), but then it is later // added when the search finishes _state_changed = true; _partial_range.front()->remove_finding(pos); } } virtual void append_line([[maybe_unused]] LogLines* ll, size_t pos, Line* line) override { unique_lock ul(_m); if (!_finished) return; assert(ll == &_ll); assert(pos >= *_finished); if (_match.is_match(line->view())) { // add the current position to results assert(_partial_range.size() == 1); _partial_range.front()->post_end(pos); } } virtual void insertion([[maybe_unused]] LogLines* ll, size_t pos, size_t amount) override { unique_lock ul(_m); assert(ll == &_ll); _state_changed = true; for (auto &x : _partial_range) { x->insert_or_delete_lines(pos, amount, true); } } virtual void deletion([[maybe_unused]] LogLines* ll, size_t pos, size_t amount) override { unique_lock ul(_m); assert(ll == &_ll); _state_changed = true; for (auto &x : _partial_range) { x->insert_or_delete_lines(pos, amount, false); } } /* LogLines is clearing all the data in the filter */ virtual void clear_lines([[maybe_unused]] LogLines* ll) override { unique_lock ul(_m); assert(ll == &_ll); _state_changed = true; assert(_partial_range.size() == 1); _partial_range.front()->clear(); _finished = 0; } // accessor for the string value operator string() const { return _keyword; } /* Sets what the user has currently typed for the keyword. This can be * the same as before with a new letter added or a letter removed. It * need not be at the end of the previous word (e.g., cursor pos). */ virtual void current_type(const string& typed) { unique_lock ul(_m); _state_changed = true; // step 1: scan the word from the start to look for differences size_t i = 0; while (i < typed.length()) { // words differ in middle due to insertion or mid-way // deletion. remove all partial ranges after the // difference if (i < _keyword.length() && typed[i] != _keyword[i]) { // truncate keyword to prefix of typed _keyword = _keyword.substr(0, i); // pop extra partial ranges while (_partial_range.size() > i) { _partial_range.pop_front(); } } // if we still have more typed and its longer than // keyword, treat them as appends if (i >= _keyword.length()) { assert(i == _keyword.length()); _keyword += typed[i]; _partial_range.push_front(make_unique( G::SEARCH_RANGE)); } ++i; } // if we processed all of typed and it matched keyword, then // _keyword is longer and updated was to remove a character if (_keyword != typed) { assert(_keyword.length() > typed.length()); _keyword = typed; while (_partial_range.size() > _keyword.length()) { _partial_range.pop_front(); } } _match.commit_keyword(_keyword); } /* tell where the user's current line number is so that we can prioritize * searching around that position */ virtual inline void set_whence(size_t whence) { unique_lock ul(_m); assert(whence <= G::EOF_POS || whence == G::NO_POS); _whence = whence; } /* called when the user has finished typing the keyword. we can get rid * of all the per-character searches we won't need them now */ virtual inline void finish() { auto lock = _ll.read_lock(); unique_lock ul(_m); _state_changed = true; // there weren't any characters if (_partial_range.size() == 0) { _exit = true; return; } assert(!_keyword.empty()); // remove all explored ranges for strict prefixes of keyword while (_partial_range.size() >= 2) { _partial_range.pop_back(); } // mark the end of log lines and register a listener for new // updates size_t end = _ll.length_locked(); _finished = end; _partial_range.front()->mark_end(end); _match.commit_keyword(_keyword); } /* accessor for the keyword */ virtual inline string get_keyword() const { unique_lock ul(_m); return _keyword; } /* indicates a match was found after the end of log lines, e.g., * because we are tailing new appends */ virtual inline void post_end(size_t pos) { unique_lock ul(_m); assert(_partial_range.size() == 1); _partial_range.front()->post_end(pos); } /* for debug tracing out contents */ virtual inline void trace(ostream& out) { unique_lock ul(_m); assert(_partial_range.size()); _partial_range.front()->trace(out); } /* calls next_match on the front partial range, reflecting the current * keyword */ virtual inline size_t next_match(size_t whence, bool dir) { unique_lock ul(_m); assert(_partial_range.size()); return _partial_range.front()->next_match(whence, dir); } /* returns true if the current keyword is empty so we have nothing in * the explored range sets */ virtual inline bool empty() { unique_lock ul(_m); if (_partial_range.empty()) { assert(_keyword.empty()); return true; } assert(!_keyword.empty()); return false; } /* takes a set of positions, and intersects them with those that match * our keyword */ virtual inline void conjunctive_join(set* lines) const { unique_lock ul(_m); if (!_partial_range.size()) return; _partial_range.front()->conjunctive_join(lines); } /* takes a set of positions, and unions them with those that match our * keyword */ virtual inline void disjunctive_join(set* lines) const { unique_lock ul(_m); if (!_partial_range.size()) return; _partial_range.front()->disjunctive_join(lines); } /* calls next_range on the exploreod range */ virtual inline size_t next_range(size_t whence, bool dir) { unique_lock ul(_m); assert(_partial_range.size()); return _partial_range.front()->next_range(whence, dir); } /* returns true if pos is a match we've identified */ virtual inline bool is_match(size_t pos) { unique_lock ul(_m); assert(_partial_range.size()); return _partial_range.front()->is_match(pos); } /* used to generate the line of information regarding current keywords * being sought. ellipsis are used to indicate that the search is * ongoing */ virtual string get_description() const { unique_lock ul(_m); string ret = _match.get_description(); if (_finished && (!_partial_range.front()->completed())) { ret += ".."; } return ret; } virtual bool completed() const { unique_lock ul(_m); if (!_finished) return false; return _partial_range.front()->completed(); } protected: /* main searching thread. Acquires a read lock from loglines, gets the * current whence as the starting point, and asks for a range. It then * matches on that range and tells explored_range the results. When we * are finished searching, or we are being destructed, it exits */ virtual void searcher() { unique_lock lock(_m); while (!_exit) { size_t start, end; if (_partial_range.size() > 0) { size_t whence = _whence; // search at the end of the log, unless we've // already set the finish line // avoid deadlock lock.unlock(); if (whence == G::NO_POS) whence = _ll.length(); lock.lock(); if (_finished != nullopt && whence >= *_finished) whence = *_finished; // cache front range in case it changes during // unlocking while searching ExploredRange* range = _partial_range.front().get(); if (range->completed()) break; range->explore(whence, &start, &end); set results; size_t lines = 0; // our matching callback to pass loglines function fn = bind(&Match::is_match, _match, _1); // we will unlock for the duration of the match. // however if something changes that would make // our results invalid (e.g., line insertions) // just abandom these results and try again _state_changed = false; lock.unlock(); lines = _ll.range_add_if(start, end, &results, fn); if (_exit) [[unlikely]] break; lock.lock(); // check if anything changed such that we should // ignore this search results if (_state_changed == true) [[unlikely]] continue; if (end > lines) end = lines; if (start != end) { range->extend(start, end, results); } // if we have the end position and searched it // all, this thread can finish if (_finished != nullopt && _partial_range.front().get() == range && _partial_range.front()->completed()) break; } lock.unlock(); this_thread::sleep_for(chrono::milliseconds(1)); lock.lock(); } } // used to retrieve line values and register for update callbacks const LogLines& _ll; // encapsulates the search parameters Match _match; // keywor being sough string _keyword; // this list is per character in the search word so that searching is // responsive as the characters come in, and partial results are useful // when backspace is hit list> _partial_range; // for thread safety mutable mutex _m; // signals to worker thread this class is being deconstructed if it is // still searching bool _exit; // current line being viewed, to priorize searching so the user sees // relevant lines sooner size_t _whence; // searching thread unique_ptr _worker; // size of loglines at the moment the keysearch search is locked in and // a listener is registered optional _finished; // used to signal the worker thread that the state changed such that its // search results may be invalid or the range it wants to send them to // is gone atomic _state_changed; }; #endif // __LINE_FILTER_KEYWORD__H__ logserver/src/line_intelligence.h000066400000000000000000000475041512456611200175140ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __LINE_INTELLIGENCE__H__ #define __LINE_INTELLIGENCE__H__ #include #include #include #include #include "base64.h" #include "constants.h" #include "line.h" #include "tokenizer.h" #include "zcat.h" using namespace std; /* LineIntelligence is a static class that performs heuristics on lines to * decide whether thnigs can be decoder, such as hex encoding, base64 encoding, * timestamp to human time, etc. */ class LineIntelligence { public: /* make a 4 spaces times input arg */ static string make_tabs(size_t tabs) { string ret; for (size_t i = 0; i < tabs; ++i) { ret += " "; } return ret; } /* Indent format brace-structured code */ /* TODO: this is just to have something now, but obviously a better * parsing of the language tokens is needed to make it pretty */ static void process_code(const string_view& data, list* tokens) { string quotes; Tokenizer::annotate_quote(data, "es); size_t tabs = 0; size_t last = 0; size_t cur = 0; assert(data.size() == quotes.size()); while (cur < data.size()) { if (quotes[cur] == '\'') { // ignore punctuation } else { char c = data[cur]; if (c == ';' || c == '{' || c == '}') { if (c == '}' && tabs) --tabs; tokens->push_back( make_tabs(tabs) + string(data.substr( last, cur + 1 - last))); last = cur + 1; if (c == '{') ++tabs; } } ++cur; } if (last != cur) { tokens->push_back(string(data.substr(last))); } } /* pretty print JSON data into a lines with tabs */ static void process_json(const string_view& data, list* tokens) { string quotes; Tokenizer::annotate_quote(data, "es); size_t tabs = 0; size_t last = 0; size_t cur = 0; assert(data.size() == quotes.size()); while (cur < data.size()) { if (quotes[cur] == '\'') { // ignore punctuation in quoted segments } else { char c = data[cur]; // open of array or object or end of current if (c == '[' || c == '{' || c == ',') { string token = make_tabs(tabs); token += Tokenizer::trim( data.substr(last, cur + 1 - last)); tokens->push_back(token); last = cur + 1; if (c == '{' || c == '[') ++tabs; // close of array or object } else if (c == '}' || c == ']') { if (last < cur) { string token = make_tabs(tabs); token += Tokenizer::trim( data.substr( last, cur - last)); tokens->push_back(token); } if (cur + 1 < data.size() && data[cur + 1] == ',') { ++cur; } last = cur; if (tabs) --tabs; string suffix = make_tabs(tabs); suffix += Tokenizer::trim( data.substr(last, cur + 1 - last)); tokens->push_back(suffix); last = cur + 1; } } ++cur; } if (last != cur) { tokens->push_back(string(data.substr(last))); } } /* split a string by deliminator intok a list of tokens */ static void split_with_empty(const string_view& data, const string& deliminator, list* tokens) { size_t pos = 0; while (true) { size_t start = pos; pos = data.find(deliminator, start); if (pos == string::npos) { tokens->push_back(data.substr(start)); break; } tokens->push_back(data.substr(start, pos - start)); pos += deliminator.length(); } } /* find and replace all occurances on a string and return result */ static string replace(const string_view& data, const string& find, const string& replacement) { list pieces; split_with_empty(data, find, &pieces); if (pieces.size() < 2) return string(data); stringstream ss; auto it = pieces.begin(); ss << *it; ++it; while (it != pieces.end()) { ss << replacement << *it; ++it; } return ss.str(); } static bool heuristic_hex_unescape(const string_view& data) { int hits = 0; int misses = 0; for (size_t i = 0; i < data.length(); ++i) { if (data[i] == '%' && i + 2 < data.length()) { if (isxdigit(data[i + 1]) && isxdigit(data[i + 2])) { hits += 1; } else { misses += 1; } } } if (!misses && hits) return true; if (hits > 10 && misses < 4) return true; return false; } static string hex_unescape(const string_view& data) { string ret; for (size_t i = 0; i < data.length(); ++i) { if (data[i] == '%' && i + 2 < data.length()) { if (isxdigit(data[i + 1]) && isxdigit(data[i + 2])) { ret += dehexify(data.substr(i + 1, 2)); i += 2; continue; } } ret += data[i]; } return ret; } /* main function to split a string and return the result as new lines to * insert into the log_lines object. If param c is set, then it splits * using that character as the line break (i.e., if user specifies what * char to use). Otherwise it applies heuristics: does this look like * json, code, http query, etc., then split appropriately. Otherwise * count occurances of possible delimiters, such as comma, \n, |, etc., * and split on that. Finally just split with a reasonable ragged right * to avoid breaking up words. */ // TODO: pass in as Line and preserve the node for GEB static list> split(const string_view& line, char c) { // depending on the type of splitting we do, either we have // string_views or strings, but not both. list to_add; list string_to_add; // list of new Line objects to add list> ret; if (line.empty()) { // TODO: no make ret.emplace_back(make_unique(line)); return ret; } /* if we know what char to use, then do that */ if (c != '\0') { split_with_empty(line, string(1, c), &to_add); /* else apply heuristics */ } else { map counts; count_occurrences(line, &counts); if (heuristic_newlines(line, counts)) { split_with_empty(line, "\\n", &to_add); } else if (heuristic_httparg(line, counts)) { split_with_empty(line, "&", &to_add); } else if (heuristic_pipe(line, counts)) { split_with_empty(line, "|", &to_add); } else if (heuristic_json(line, counts)) { process_json(line, &string_to_add); } else if (heuristic_code(line, counts)) { process_code(line, &string_to_add); } // if no heuristic triggered then both lists will be // empty. If a heuristic did trigger, but triggering // didn't result in it actually adding new lines, then // one of the lists is empty and the other contains one // element. Either way we need to break it the old // fashion way. if (string_to_add.size() + to_add.size() <= 1) { ret.clear(); to_add.clear(); string_to_add.clear(); // TODO: use actual screen width instead size_t i = G::LINE_WIDTH; string_view cur = line; while (i < cur.length()) { i = rewind(cur, i, 20); to_add.emplace_back(cur.substr(0, i + 1)); cur = cur.substr(i + 1); i = G::LINE_WIDTH; } if (!cur.empty()) to_add.emplace_back(cur); } } for (const auto& x : to_add) { ret.push_back(make_unique(x)); } for (const auto& x : string_to_add) { ret.push_back(make_unique(x)); } return ret; } /* find a nice place to break a string, given that we want to break it * at the parameter pos position. look back the parameter amount * positions in the string for an obvious break pos */ static size_t rewind(const string_view& line, size_t pos, size_t amount) { static set good_break = { '&', ' ', '-', '_', '#', ',', '\t', ')', '(', '{', '}', '<', '>', ':', ';', '!', '?', '.', '\"', '\'', '\n'}; assert(pos < line.length()); assert(amount < pos); for (size_t i = 0; i < amount; ++i) { char c = line[pos - i]; if (good_break.count(c)) return pos -i; } return pos; } /* given a line, try to see if there are ways we can decode it, such as * base64, base16, and unix timestamps to human. the parameter * frustration is the number of times the user has repeated tried to * call this function, so loosen the heuristics for success to encourage * trying harder */ static optional apply_heuristics(const string_view& line, int frustration) { // ret is what is returned, cur is the result of current // heuristic, which is nullopt if not applicable. optional ret = nullopt; optional cur = nullopt; ret = LineIntelligence::useful_timestamp(line); cur = LineIntelligence::useful_base16( ret ? *ret : string(line), frustration); if (cur) ret = cur; cur = LineIntelligence::useful_base64( ret ? *ret : string(line), frustration); if (cur) ret = cur; if (heuristic_hex_unescape(ret ? *ret : line)) { cur = LineIntelligence::hex_unescape( ret ? *ret : line); } if (cur) ret = std::move(cur); return ret; } /* looks for unix timestamps in line and returns them converted to human * time */ static optional useful_timestamp(const string_view& line) { regex r_ts("([^0-9]|^)([0-9]{13}|[0-9]{10})($|[^0-9])"); smatch sm; string copy(line); auto it = copy.cbegin(); map replaces; while (regex_search(it, copy.cend(), sm, r_ts)) { string result = ts_to_human(sm[2].str()); if (!result.empty()) replaces[sm[2].str()] = result; it += sm.position() + 10; } return apply_matches(line, replaces); } /* takes a string and returns a dictionary of find-replace pairs applied * to it. returns nullopt if there is nothing to replace */ static optional apply_matches( const string_view& line, const map& replaces) { if (replaces.empty()) return nullopt; string ret; bool first = true; for (const auto &x: replaces) { assert(line.find(x.first) != string::npos); if (first) { ret = replace(line, x.first, x.second); first = false; } else { ret = replace(ret, x.first, x.second); } } assert(ret != line); return ret; } /* hex decode the parameter in_line if heuristics pass. return nullopt * if no hex was found */ static optional useful_base16(const string& in_line, int frustration) { string line = " " + in_line + " "; regex r_b64("[ x=:;,.'\\\"\\t\\r]([A-Fa-f0-9]+)[ :;,.'\\\"\\t\\r]"); smatch sm; auto it = line.cbegin(); map replaces; if (frustration > 4) frustration = 4; assert(frustration >= 0); static const size_t minlen[] = {7, 7, 5, 5, 3}; static const size_t minpercent[] = {100, 95, 75, 50, 25}; while (it != line.cend() && regex_search(it, line.cend(), sm, r_b64)) { if (sm[1].str().length() > minlen[frustration]) { // dehex the finding, if it is gzipped ungzip // it. then check against the heuristic of // printable to keep it string result = dehexify(sm[1].str()); if (ZCat::magic(result)) result = ZCat::zcat(result); if (percent_printable(result, false) < minpercent[frustration]) result = ""; if (!result.empty()) replaces[sm[1].str()] = result; } for (size_t i = 0; i < 4; ++i) { if (it != line.cend()) ++it; } } if (replaces.empty()) { return nullopt; } for (const auto &x: replaces) { line = replace(line, x.first, x.second); } assert(line.size() >= 2); return line.substr(1, line.length() - 2); } /* dehex the string data, return empty string if the percent printable * is less then parameter percent */ static string dehexify(const string &data, size_t percent) { string ret = dehexify(data); if (percent_printable(ret, false) >= percent) return ret; return ""; } /* dehex the input string data */ static string dehexify(const string_view& data) { if (data.length() % 2) { string tmp = "0" + string(data); return dehexify(tmp); } stringstream ssout; for (size_t i = 0; i < data.length(); i += 2) { stringstream ss; int value; ss << hex << data.substr(i, 2); ss >> value; ssout << (char) value; } return ssout.str(); } /* search for base64 segments that produce printable sequences based on * heuristics and parameter frustration. return nullopt if nothing found * to decode */ static optional useful_base64(const string& in_line, int frustration) { string line = " " + in_line + " "; regex r_b64("[ =:;,.'\\\"\\t\\r]([-A-Za-z0-9+/_\\\\]+={0,2})[ :;,.'\\\"\\t\\r]"); smatch sm; auto it = line.cbegin(); map replaces; if (frustration > 4) frustration = 4; assert(frustration >= 0); static const size_t minlen[] = {7, 7, 5, 5, 3}; static const size_t minpercent[] = {100, 95, 75, 50, 25}; while (it != line.cend() && regex_search(it, line.cend(), sm, r_b64)) { // TODO constify the 7, min length of base 64 if (sm[1].str().length() > minlen[frustration]) { string result = b64_nonbinary(sm[1].str(), minpercent[frustration]); if (!result.empty()) replaces[sm[1].str()] = result; } for (size_t i = 0; i < 4; ++i) { if (it != line.cend()) ++it; } } if (replaces.empty()) return nullopt; for (const auto &x: replaces) { line = replace(line, x.first, x.second); } assert(line.length() >= 2); return line.substr(1, line.length() - 2); } /* base64 decode the string parameter s and return the result if more * than minpercent percent of the characters are printable. also gunzips * it if it is clearly gzipped data */ static string b64_nonbinary(const string& s, size_t minpercent) { string val = replace(s, "\\n", ""); for (size_t i = 0; i < 4; ++i) { // only for long base64 segments if (s.size() < 15 && i) break; string ret = ::Base64::decode(val); // TODO: instead insert as new lines if (ZCat::magic(ret)) ret = ZCat::zcat(ret); if (percent_printable(ret, true) >= minpercent) return ret; val = "A" + val; } return ""; } /* returns the percent of printable characters in s, used to assess if * base64 decoding was somewhat reasonable */ static size_t percent_printable(const string& s, bool ignore_start) { if (s.empty()) return 100; size_t count = 0; size_t pos = 0; if (ignore_start) { while (!isspace(s[pos]) && !isprint(s[pos])) ++pos; } if (pos == s.size()) return 0; size_t start_pos = pos; while (pos < s.size()) { if (isspace(s[pos]) || isprint(s[pos])) { ++count; } ++pos; } return count * 100 / (s.length() - start_pos); } /* returns the count of the char needle in the string haystack */ static size_t count_occurrences(const string_view& haystack, char needle) { size_t ret = 0; for (size_t i = 0; i < haystack.length(); ++i) { if (haystack[i] == needle) ++ret; } return ret; } /* returns the count of the string needle in the string haystack. it * shifts search by needle size and does not count substrings giving * multiple matches */ static size_t count_occurrences(const string_view& haystack, const string& needle) { size_t i = 0; size_t pos = 0; while (true) { pos = haystack.find(needle, pos); if (pos == string::npos) return i; ++i; pos += needle.size(); } } /* takes a string haystack and counts the number of 1 or 2 character * sequences. this is used for heuristics like does this string have * many \n sequences, or other types of punctuation. the counts map has * to have an entry for the sequence, otherwise it is ignored */ static void count_occurrences(const string_view& haystack, map* counts) { size_t max_len = 0; if (!counts) return; if (counts->empty()) { *counts = { {"\\n", 0}, {"&", 0}, {"|", 0}, {"=", 0}, {"!=", 0}, {"==", 0}, {"{", 0}, {"}", 0}, {";", 0}, {",", 0}, {":", 0}, {"[", 0}, {"]", 0}, {"(", 0}, {")", 0}, {"\"", 0} }; max_len = 2; } else { for (const auto& x : *counts) { if (x.first.length() > max_len) { max_len = x.first.length(); } } } for (size_t i = 0; i < haystack.length(); ++i) { for (size_t j = 1; j <= max_len; ++j) { string key(haystack.substr(i, j)); if (counts->count(key)) { (*counts)[key]++; } } } } /* simple similarity heuristic for two counts. */ static bool similar(const map& counts, const string& one, const string& two) { size_t c1 = counts.at(one); size_t c2 = counts.at(two); // equal or no matches if (c1 == c2) return true; // none of one or other if (!c1 || !c2) return false; if (c1 > c2) { c1 = c2; c2 = counts.at(one); } // equal from above assert(c1 < c2); c1 += 0.15 * c2 + 5; return (c1 > c2); } /* returns true if the string is likely JSON */ static bool heuristic_json([[maybe_unused]] const string_view& line, const map& counts) { // only process JSON arrays and objects if (counts.at("[") == 0 && counts.at("{") == 0) return false; if (!similar(counts, "[", "]")) return false; if (!similar(counts, "{", "}")) return false; size_t quotes = counts.at("\""); if (quotes > 2 * counts.at("{")) return true; if (quotes > 2 * counts.at(":")) return true; if (quotes > 2 * counts.at(",")) return true; return false; } /* returns true if the string could be brace and semicolon style code */ static bool heuristic_code([[maybe_unused]] const string_view& line, const map& counts) { if (counts.at(";") > 100 && counts.at("{") > 10 && counts.at("}") > 10 && counts.at("=") > 20) return true; return false; } /* returns true if the string has lots of \n in it */ static bool heuristic_newlines(const string_view& line, const map& counts) { size_t len = line.length(); size_t newlines = counts.at("\\n"); return (newlines * 100 / len); } /* returns true if the string looks like it is |-separated */ static bool heuristic_pipe(const string_view& line, const map& counts) { size_t len = line.length(); size_t pipes = counts.at("|"); return (pipes * 150 / len); } /* returns true if the string looks like an http query string */ static bool heuristic_httparg(const string_view& line, const map& counts) { size_t len = line.length(); size_t ampers = counts.at("&"); if (ampers < 3) return false; size_t equals = counts.at("="); if (ampers * 50 / len) return true; if (equals <= ampers + 5 && 2 * equals > ampers) return true; return false; } /* convert unix time to human time */ static string ts_to_human(const string& timestamp) { struct timeval tv; gettimeofday(&tv, nullptr); size_t ts = tv.tv_sec; size_t ours = 0; time_t ours_parm = 0; size_t ours_s = 0; try { ours = stoull(timestamp); } catch (const logic_error& e) { return ""; } if (timestamp.length() == 16) { ours_s = ours / 1000000; } else if (timestamp.length() == 13) { ours_s = ours / 1000; } else { assert(timestamp.length() == 10); ours_s = ours; } string result; if ((ts >= ours_s && ts < ours_s + 315360000) || (ours_s >= ts && ours_s < ts + 315360000)) { // within a decade ours_parm = static_cast(ours_s); result = asctime(gmtime(&ours_parm)); if (result.length()) result = result.substr(0, result.length() - 1); if (result.length() > 6) { stringstream ss_ms; ss_ms << (ours % 1000); result = result.substr(0, result.length() - 5) + "." + ss_ms.str() + result.substr(result.length() - 5); } } return result; } }; #endif // __LINE_INTELLIGENCE__H__ logserver/src/log_lines.h000066400000000000000000000554651512456611200160230ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __LOG_LINES__H__ #define __LOG_LINES__H__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "base64.h" #include "config.h" #include "constants.h" #include "directory_line_provider.h" #include "event_bus.h" #include "fd_line_provider.h" #include "file_line_provider.h" #ifdef __USE_GEB__ #include "gdb_line_provider.h" #include "gdb_node_line_provider.h" #endif // __USE_GEB__ #include "huge_vector.h" #include "i_line_provider.h" #include "line.h" #include "line_intelligence.h" #include "match.h" #include "run.h" #include "static_demo.h" #include "static_help.h" #include "xref.h" using namespace std; /* LogLines stores and controls access to the actual lines of data being * displayed. This includes operations on lines such as editing, breaking, and * decoding, and supporting xrefs for lines. Each log lines has a line provider, * which sources the lines themselves and adds them to the log, or is statically * prebuilt with the relevant lines. */ class LogLines : public ILogLines { public: // default constructor, used in this class for custom initializing LogLines() : _type(LL_NONE) { init(); } /* static log lines that displays the lines in parameter lines. */ LogLines(const vector& lines, const string& display_name) : _display_name(display_name), _type(LL_SYNTHETIC) { init(); for (const auto& x : lines) { add_line(x); } } /* creates a loglines based on the contents of a filename in parameter * name */ explicit LogLines(const string& name) : _filename(name), _display_name(name) { if (filesystem::is_directory(name)) { _type = LL_DIRECTORY; init(); _lp.reset(new DirectoryLineProvider(this, name)); _lp->start(); } else { _type = LL_FILE; init(); _lp.reset(new FileLineProvider(this, name)); _lp->start(); } } /* create a log lines by taking a file descriptor and streaming its * lines to the loglines. This occurs when something is piping into * logserver at startup */ explicit LogLines(int fd) : _display_name("stdin"), _type(LL_STDIN) { init(); _lp.reset(new FDLineProvider(this, fd)); _lp->start(); } /* construct a loglines by running a command feeding an old loglines * into it and producing the output as a new loglines */ explicit LogLines(Run* run) : _display_name(run->command()), _type(LL_PIPE) { init(); _runner.reset(run); _lp.reset(new FDLineProvider(this, _runner->read_fd())); _lp->start(); } #ifdef __USE_GEB__ LogLines(TGraph* gdb, const string& display_name, TGraph::Node* where) : _display_name(display_name), _gdb(gdb) { // if displayname is not empty we are showing a file using the // next and prev relation. otherwise we are splaying out a node if (display_name.empty()) { assert(where); _type = LL_GDB_NODE; init(); _lp.reset(new GdbNodeLineProvider(this, gdb, where)); } else { _type = LL_GDB; init(); _lp.reset(new GdbLineProvider(this, gdb, where)); } _lp->start(); } #endif // __USE_GEB__ /* shared initialization for all constructors */ void init() { unique_lock ul = write_lock(); #ifdef __USE_GEB__ _gdb = nullptr; #endif // __USE_GEB__ _lines.clear(); EventBus::_()->clear_lines(this); _eof = false; } // default destructor virtual ~LogLines() { EventBus::_()->eventmaker_finished(this); unique_lock ul = write_lock(); _lp.reset(nullptr); _lines.clear(); } /* create a log lines consisting of the static data for a static page, * like the help screen */ static LogLines* show_static(const string& name) { unique_ptr ret = make_unique(); ret->_type = LL_STATIC; ret->_display_name = name; if (name == StaticHelp::NAME) StaticHelp::render(ret.get()); if (name == StaticDemo::NAME) StaticDemo::render(ret.get()); return ret.release(); } /* returns true if this is a static page like the help screen */ virtual bool is_static(const string& name) const { if (_type != LL_STATIC) return false; return _display_name == name; } // locks and returns the line at parameter pos virtual inline string_view get_line_unlocked(size_t pos) const { shared_lock ul(_m); return get_line(pos); } virtual inline Line* get_line_object_unlocked(size_t pos) const { shared_lock ul(_m); return get_line_object(pos); } virtual inline Line* get_line_object(size_t pos) const { return _lines.at(pos).get(); } // returns the line at parameter pos. requires a read_lock() held virtual inline string_view get_line_locked(size_t pos) const { return get_line(pos); } // sets the line at pos to the value val virtual inline void set_line_unlocked(size_t pos, const string& val) { unique_lock ul = write_lock(); return set_line(pos, val); } // appends a batch of new Lines to the log lines virtual void add_lines(list* lines) override { unique_lock ul = write_lock(); while (lines->size()) { Line* line = lines->front(); lines->pop_front(); _lines.add(unique_ptr(line)); EventBus::_()->append_line(this, _lines.length() - 1, line); } } // appends a new Line to the log lines virtual size_t add_line(Line* line) override { unique_lock ul = write_lock(); _lines.add(unique_ptr(line)); EventBus::_()->append_line(this, _lines.length() - 1, line); return _lines.length() - 1; } // adds a new string to the log lines virtual size_t add_line(const string& line) override { unique_lock ul = write_lock(); Line* new_line = new Line(line); _lines.add(unique_ptr(new_line)); EventBus::_()->append_line(this, _lines.length() - 1, new_line); return _lines.length() - 1; } /* search [start, end) with the matching parameters */ virtual size_t range_add_if(size_t start, size_t end, set* results, function fn) const { shared_lock ul(_m); return _lines.range_add_if(start, end, results, fn); } // inserts a line at a specific position in the log lines */ virtual size_t insert_line(const string& line, size_t pos) { unique_lock ul = write_lock(); Line* new_line = new Line(line); _lines.insert(unique_ptr(new_line), pos); EventBus::_()->insertion(this, pos, 1); EventBus::_()->edit_line(this, pos, new_line); return pos; } virtual pair get_range_unlocked(size_t pos, size_t radius) { size_t start, end; if (pos < radius) start = 0; else start = pos - radius; if (!length()) return make_pair(0, 0); if (pos + radius >= length()) end = length() - 1; else end = pos + radius; return make_pair(start, end); } // TODO: refactor, only info a line. I should start a thread that infos // all the lines. virtual void info(size_t pos, bool full, int frustration) { if (pos == G::NO_POS) return; if (full) { pair range = get_range_unlocked( pos, 20 * (frustration + 1)); for (size_t i = range.first; i < range.second; ++i) { info(i, false, frustration >> 1); } return; } string_view line; { shared_lock ul(_m); line = get_line(pos); } optional ret = LineIntelligence::apply_heuristics( line, frustration); if (ret) { unique_lock ul = write_lock(); set_line(pos, *ret); split_locked(pos, '\n'); } } /* gives the line at position pos, is there an xref to follow? if there * is more than one, or display_all is true, then create a synthetic * loglines that lists those options. Otherwise jump to the unique * target. Returns a pair of loglines and line number, or (nullptr, 0) * if there is no xref to follow */ virtual pair follow(size_t pos, bool display_all) { // jumps have to have a valid line if (pos == G::NO_POS) return make_pair(nullptr, 0); // special handler to perform the jump to the demo page in help if (_type == LL_STATIC && _display_name == StaticHelp::NAME) { const string_view& line = get_line(pos); if (line.find("@DEMOTIME") != string::npos) { return make_pair(show_static("demo"), 0); } } // use the xref to get the jump auto ret = follow_impl(pos, display_all); /* signal on the synthetic page that we have taken this path, to * help with analysis of all options */ if (ret.first != nullptr && _type == LL_SYNTHETIC) { unique_lock lock = write_lock(); mark(pos, 0, 'x'); } /* if there is no xref, check if the line itself has a xref * format we use, such as a grep -rn line or an entry in a file * we already added a comment for */ if (ret.first == nullptr) { const string_view& line = get_line(pos); string file; size_t lineno = G::NO_POS; if (XRef::grep_line(line, &file, &lineno) || XRef::storytime_line(line, &file, &lineno) || XRef::link_line(line, &file, &lineno) || XRef::ctags_line(_filename, line, &file, &lineno)) { return make_pair( new LogLines(file), lineno); } } return ret; } // split a long line based on heursitics virtual void split(size_t pos) { return split(pos, '\0'); } // splits a long line by the character c as delimiter, unless c == '\0' // in which case apply relevant heuristics virtual void split(size_t pos, char c) { unique_lock ul = write_lock(); split_locked(pos, c); } // implement split with the lock held virtual void split_locked(size_t pos, char c) { if (!exists_line_locked(pos)) return; list> to_add; to_add = LineIntelligence::split(get_line(pos), c); assert(to_add.size() > 0); unique_ptr last = std::move(to_add.back()); assert(last.get()); to_add.pop_back(); // TODO: split should allow reassembly with backspace by storing // in the Line type with the lines to delete size_t amount = to_add.size(); _lines.insert(to_add, pos); // note to_add is moved at this point EventBus::_()->insertion(this, pos, amount); for (size_t i = pos; i < pos + amount; ++i) { EventBus::_()->edit_line(this, i, _lines[i].get()); } _lines[pos + amount] = std::move(last); EventBus::_()->edit_line(this, pos + amount, _lines[pos + amount].get()); } // acquires the shared lock for the loglines virtual shared_lock read_lock() const { return shared_lock(_m); } // acquires the unique lock for the loglines virtual unique_lock write_lock() const { #ifdef __APPLE__ unique_lock lock(_m, defer_lock); while (!lock.try_lock()) { this_thread::yield(); } return lock; #else return unique_lock(_m); #endif } virtual void merge(size_t pos) { if (pos == G::NO_POS) [[unlikely]] return; unique_lock ul = write_lock(); if (pos + 1 >= length_locked()) [[unlikely]] return; (*_lines[pos]) += (*_lines[pos + 1]); EventBus::_()->edit_line(this, pos, _lines[pos].get()); _lines.remove(pos + 1); EventBus::_()->deletion(this, pos + 1, 1); } // Removes position pos from the log lines virtual void remove(size_t pos) { unique_lock ul = write_lock(); _lines.remove(pos); EventBus::_()->deletion(this, pos, 1); } /* Returns a new loglines consistent of the tabbed data with column * suppression */ virtual LogLines* tabfilter_clone( const string& name, char tab_char, set suppress_cols) { unique_lock ul = write_lock(); unique_ptr ret = make_unique(); ret->_type = LL_PERMAFILTER; ret->_display_name = name; for (size_t i = 0; i < _lines.length(); ++i) { ret->add_line(_lines[i]->filter_tabs(tab_char, suppress_cols)); } return ret.release(); } /* Returns a new loglines consisting of all the lines between the two * nearest pin locations up and down from the cursor. Caller responsible * to delete */ virtual LogLines* pinfilter_clone( const string& name, optional pin_up, optional pin_down) { unique_lock ul = write_lock(); unique_ptr ret = make_unique(); ret->_type = LL_PERMAFILTER; ret->_display_name = name; if (!pin_up) pin_up = 0; else ++*pin_up; if (!pin_down) pin_down = _lines.length(); assert(*pin_up < *pin_down || (*pin_up == 0 && *pin_down == 0)); assert(*pin_down <= _lines.length()); for (size_t i = *pin_up; i < *pin_down; ++i) { ret->add_line(string(get_line(i))); } return ret.release(); } // caller responsible to delete virtual LogLines* permafilter_clone(set& lines, const string& name) { unique_lock ul = write_lock(); unique_ptr ret = make_unique(); ret->_type = LL_PERMAFILTER; ret->_display_name = name; for (const auto &x : lines) { ret->add_line(string(get_line(x))); } ret->add_line(""); return ret.release(); } // returns number of lines in log lines virtual size_t length() const { shared_lock ul(_m); return length_locked(); } // returns number of lines in log lines, assumes lock is held virtual size_t length_locked() const { return _lines.length(); } /* Requires that keyword is lower case */ virtual size_t find(size_t cur, size_t tab, const string& keyword) const { if (!_lines.valid(cur)) return string::npos; shared_lock ul(_m); return G::to_lower(get_line(cur)).find(keyword, tab); } /* Requires that keyword is lower case */ virtual size_t rfind(size_t cur, size_t tab, const string& keyword) const { if (!_lines.valid(cur)) return string::npos; shared_lock ul(_m); return G::to_lower(get_line(cur)).rfind(keyword, tab); } /* Writes the entire current log lines to a file that is passed in, or a * default file otherwise. * */ virtual void save(const string& filename) const { shared_lock ul(_m); ofstream fout; if (filename.empty()) { fout.open(Config::_()->get("initcwd") + "/LOGSERVER_FILE"); } else if (filename[0] == '/') { fout.open(filename); } else { fout.open(Config::_()->get("initcwd") + "/" + filename); } if (!fout.good()) fout.open("/tmp/LOGSERVER_FILE"); _lines.write(fout); } /* saves the current line to a file TODO: if navi cur above middle line, should it be the middle line? */ virtual void save_line(const string& filename, size_t line) const { if (line == G::NO_POS) return; shared_lock ul(_m); ofstream fout; if (filename.empty()) { fout.open(Config::_()->get("initcwd") + "/LOGSERVER_LINE"); } else if (filename[0] == '/') { fout.open(filename); } else { fout.open(Config::_()->get("initcwd") + "/" + filename); } if (!fout.good()) fout.open("/tmp/LOGSERVER_LINE"); fout << get_line(line) << endl; } // returns true if the eof flag has been set for the log lines virtual bool eof() const override { return _eof; } // sets the eof flag to the parameter value virtual void set_eof(bool value) override { _eof = value; } /* flush writes a line to the fd and appends a newline */ static void stream_write_line(int fd, const string_view& line) { ssize_t r = ::write(fd, line.data(), line.length()); if (r < 0) return; if (r < static_cast(line.length())) [[unlikely]] { string l = string(line.substr(r)); while (l.size()) { r = ::write(fd, l.data(), l.length()); if (r < 0) return; l = l.substr(r); } } while (true) { r = ::write(fd, "\n", 1); if (r != 0) return; } } /* writes the lines corresponding to the set of positions in the * paramter lines to the input stream for the process in parameter run. * Used to stream current loglines to a pipe process and take output as * a new loglines */ virtual void stream_write(Run* run, const set& lines) { // TODO: only write matching lines unique_lock ul = write_lock(); // TODO: refactor int fd = run->write_fd(); if (lines.size()) { for (const auto &x : lines) { stream_write_line(fd, get_line(x)); } } else { for (const auto &x : _lines) { stream_write_line(fd, x->view()); } } Config::_()->set("info", ""); run->close_write(); } /* gets a snippet of lines around the current one for tracking in the * story file */ virtual void quote(size_t cur, const set& view, size_t width, stringstream* ss) { size_t total_lines = length(); // we have a clean view, go by lines if (view.empty()) { size_t i = 0; if (cur > width) i = cur - width; for (; i < cur + width; ++i) { if (i >= total_lines) break; string_view line = get_line_unlocked(i); if (line.empty()) { ++width; } else { *ss << "\t" << line << endl; } } return; } // else we have a filtered view, use that auto x = view.lower_bound(cur); for (size_t i = 0; i < width; ++i) { if (x != view.cbegin()) --x; } for (size_t i = 0; i < 2 * width + 1; ++i) { if (x == view.cend()) break; string_view line = get_line_unlocked(*x); if (!line.empty()) { *ss << "\t" << line << endl; } else { ++width; } ++x; } } /* return the line back to original if it has changed */ virtual void revert(size_t pos) { shared_lock ul(_m); if (!exists_line_locked(pos)) return; _lines[pos]->revert(); EventBus::_()->edit_line(this, pos, _lines[pos].get()); } /* returns the display name for the log lines */ virtual string display_name() const { shared_lock ul(_m); return _display_name; } /* returns the filename for the log lines */ virtual string file_name() const { shared_lock ul(_m); return _filename; } /* when used as a media server, plays the file at this line */ virtual void enter(size_t pos) { unique_lock ul = write_lock(); if (_type == LL_DIRECTORY) { ofstream fout(_filename + "/.mediaserver", ios::app); mark(pos, 0, 'x'); string path = _lp->get_line(pos); fout << path << endl; fout.close(); string play = "mplayer -fs -msglevel all=-1 \"" + path + "\""; Run run(play); run(); run.read(200000); } } /* different types of log lines based on how it got data */ enum LL_TYPE { LL_STDIN, // read from stream LL_FILE, // read from a file LL_PERMAFILTER, // filtered view of prior log lines LL_PIPE, // read from a command pipeline output LL_SYNTHETIC, // created static list of items LL_DIRECTORY, // created from a directory list LL_GDB, // GEB summary page LL_GDB_NODE, // GEB node page LL_STATIC, // static page like help LL_NONE // no log lines }; /* returns the type of the log lines */ virtual LL_TYPE type() const { return _type; } protected: /* helper function to ensure that pos can be mapped to a real line. * assumes lock is held */ virtual bool exists_line_locked(size_t pos) { return !(pos == G::NO_POS || pos > length_locked() || !length_locked()); } /* implementation of follow. main function handles edge cases */ virtual pair follow_impl( [[maybe_unused]] size_t pos, [[maybe_unused]] bool display_all) { shared_lock ul(_m); #ifdef __USE_GEB__ if (_type == LL_GDB) { TGraph::Node* node = _lines.at(pos)->node(); if (!node) return make_pair(nullptr, 0); // TODO: in metadata auto edges = node->edges(GdbNodeLineProvider::GDB_EDGES); if (edges.empty()) return make_pair(nullptr, 0); if (edges.size() == 1 && edges.begin()->second.size() == 1) { TGraph::Node* target = *edges.begin()->second.begin(); size_t lineno = target->data().get("lineno"); // TODO: if in the same file, do it as a link if (lineno) --lineno; return make_pair( new LogLines( _gdb, "TODO", target), lineno); } // return a splay page of all edges return make_pair(new LogLines(_gdb, "", node), 4); } else if (_type == LL_GDB_NODE) { // splayed node TGraph::Node* node = _lines.at(pos)->node(); if (!node) return make_pair(nullptr, 0); size_t lineno = node->data().get("lineno"); if (!lineno) { return make_pair(nullptr, 0); } --lineno; return make_pair(new LogLines( _gdb, node->data().at(""), node), lineno); } #endif // __USE_GEB__ return make_pair(nullptr, 0); } // set the value of the line at position pos to the value val virtual inline void set_line(size_t pos, const string& val) { _lines.at(pos)->set(val); EventBus::_()->edit_line(this, pos, _lines[pos].get()); } // returns a string_view of the line at position pos virtual inline const string_view get_line(size_t pos) const { return _lines.view_at(pos); } /* edit a character in a line, used to add 'x' markers in xref for * visited links */ virtual void mark(size_t pos, size_t col, char val) { // TODO: mark the stored object if (!exists_line_locked(pos)) return; assert(col < _lines[pos]->length()); _lines.at(pos)->mark(col, val); } // returns true if this is a synthetic list of choices that the user can // select from to advance, e.g., during xref mode virtual inline bool is_choice_display() const { return _type == LL_SYNTHETIC && _display_name == _choice_name; } // the filename if this is for a directory or a file string _filename; // true if loglines is signalled by line provider that no more lines are // coming atomic _eof; // the actual line data itself, stored in a huge vector huge_vector> _lines; // shared mutex so multiple readers can progress, e.g., searching for // new data. writes are only needed for editing and inserting lines mutable shared_mutex _m; // for xref link list, the name the loglines will appear as static constexpr const char * _choice_name = "?"; // name to display in the top line string _display_name; // Run instance when running a command on the loglines unique_ptr _runner; // Interface to the instance that is feeding us lines unique_ptr _lp; // the type of the log lines, is const once first set LL_TYPE _type; #ifdef __USE_GEB__ // pointer to graph object if displaying graph data TGraph* _gdb; #endif // __USE_GEB__ }; #endif // __LOG_LINES__H__ logserver/src/logserver.cc000066400000000000000000000104351512456611200162020ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include "log_lines.h" #include "interface.h" using namespace std; namespace fs = filesystem; int stdin_process(int data_fd, int shutdown_fd) { constexpr size_t BUF_SIZE = 4096; char buf[BUF_SIZE]; setpgid(0, 0); while (true) { fd_set fds; FD_ZERO(&fds); FD_SET(STDIN_FILENO, &fds); FD_SET(shutdown_fd, &fds); int maxfd = max(STDIN_FILENO, shutdown_fd); int ret = select(maxfd + 1, &fds, nullptr, nullptr, nullptr); if (ret == 0) continue; if (ret == -1) { cerr << "select: " << strerror(errno) << endl; ::close(data_fd); ::close(shutdown_fd); return 0; } if (FD_ISSET(shutdown_fd, &fds)) { ::close(data_fd); ::close(shutdown_fd); ::close(STDIN_FILENO); return 0; } assert(FD_ISSET(STDIN_FILENO, &fds)); ssize_t read_ret = ::read(STDIN_FILENO, buf, BUF_SIZE); if (read_ret == 0) { // EOF ::close(data_fd); return 0; } if (read_ret < 0) { if (errno == EINTR) continue; cerr << "read: " << strerror(errno) << endl; ::close(data_fd); return 1; } assert(read_ret > 0); ssize_t total_written = 0; while (total_written < read_ret) { ssize_t write_ret = ::write(data_fd, buf + total_written, read_ret - total_written); if (write_ret < 0) { if (errno == EINTR) continue; cerr << "write: " << strerror(errno) << endl; ::close(data_fd); return 1; } assert(write_ret > 0); total_written += write_ret; } } return 0; } int main(int argc, char** argv) { CursesWrapper::start_win(); unique_ptr ll; #ifdef __USE_GEB__ unique_ptr graph; #endif pid_t pid = 0; int data_pipe[2] = {0, 0}; int shutdown_pipe[2] = {0, 0}; string cwd = G::realpath("."); Config::_()->set("initcwd", cwd); if (argc >= 2) { fs::path file_path = argv[1]; if (access(argv[1], R_OK)) { cerr << "Failed to open: " << strerror(errno) << endl; return -1; } else if (fs::is_directory(file_path)) { string dirname = G::realpath(argv[1]); ll.reset(new LogLines(dirname)); } else { string filename = G::realpath(argv[1]); #ifdef __USE_GEB__ Run magic("file " + filename); magic(); string result = magic.read(); if (result.find("GNU dbm") != string::npos) { // is a GDB database graph.reset(new TGraph(filename)); graph->load_db(); TGraph::Node* node = nullptr; if (argc > 2) node = graph->lookup(argv[2]); ll.reset(new LogLines(graph.get(), filename, node)); } else { ll.reset(new LogLines(filename)); } #else // not __USE_GEB__ ll.reset(new LogLines(filename)); #endif // __USE_GEB__ } } else { /* ncurses and stdin */ int r = pipe(data_pipe); if (r == -1) { throw G::errno_string("pipe(): failed", errno); } r = pipe(shutdown_pipe); pid = fork(); if (pid == -1) throw G::errno_string("fork(): failed", errno); if (pid == 0) { close(data_pipe[0]); return stdin_process(data_pipe[1], shutdown_pipe[0]); } close(data_pipe[1]); close(shutdown_pipe[0]); ll.reset(new LogLines(data_pipe[0])); } Interface interface(ll.release()); #ifdef __FUZZ_IX__ interface.fuzz(50); #else // not __FUZZ_IX__ interface.run(); #endif // __FUZZ_IX__ close(data_pipe[0]); int status; pid_t fd_pid = waitpid(pid, &status, WNOHANG); if (fd_pid == 0) { int r = write(shutdown_pipe[1], "byebye", 6); if (r == -1) { throw G::errno_string("unable to write to pipe", errno); } } close(shutdown_pipe[1]); return 0; } logserver/src/match.h000066400000000000000000000110161512456611200151240ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __MATCH__H__ #define __MATCH__H__ #include #include #include #include #include "constants.h" using namespace std; /* class that takes a line and tells you if the string matches on it. */ // TODO: can take a Line object, possible try to match on original, or would // allow folding in values into a line that can be searched for but aren't seen. // e.g., lines that have base64 decodings queued up class Match { public: explicit Match(int parms) : _parms(parms) {} virtual ~Match() {} // search defaults // look for matches static constexpr int PRESENT = 0; // match anywhere static constexpr int ANCHOR_NONE = 0; // search customizations // look for non-matches static constexpr int MISSING = 1; // match at start of line static constexpr int ANCHOR_LEFT = 2; // match at end of line static constexpr int ANCHOR_RIGHT = 4; static constexpr int CASE_SENSITIVE = 8; /* returns true if the line matches the keyword given the match * parameters stored */ bool is_match(const string_view& line) const { return (_parms & MISSING) ^ match_impl(line); } /* used to generate the line of information regarding current keywords * being sought. When the word has ! it means it is negated, if it is * preceeded by : it is left anchor, and suceeded by : then a right * anchor. Ellipses indicate that the search is not yet done */ string get_description() const { string ret; if (_parms & Match::ANCHOR_LEFT) ret += ":"; if (_parms & Match::MISSING) ret += "!"; ret += _keyword; if (_parms & Match::ANCHOR_RIGHT) ret += ":"; return ret; } /* called by line filter keyword when the user is done typing the * keyword and we now have the finalized keyword */ void commit_keyword(const string& keyword) { _keyword = keyword; check_case(); } protected: /* returns true if this is a case sensitive search */ bool inline is_case_sensitive() const { return (_parms & Match::CASE_SENSITIVE); } /* check case sets the parms to CASE_SENSITIVE if it notices that there * are any upper case letters. If a capital letter is added and removed, * the keyword stays case sensitive, to allow case sensitive lower case * search without having an extra interface key. */ void inline check_case() { if (_parms & Match::CASE_SENSITIVE) return; for (char c : _keyword) { if (c != tolower(c)) _parms |= Match::CASE_SENSITIVE; } } /* returns true if the text is neither anchored to left or right */ bool inline no_anchor() const { return (_parms & (ANCHOR_LEFT | ANCHOR_RIGHT)) == 0; } // heart of string matching. returns true if keyword is in line bool match_impl(const string_view& line) const { if (line.length() < _keyword.length()) return false; if (_keyword == "") return true; if (_parms & ANCHOR_LEFT) { if (is_case_sensitive()) return !strncmp(_keyword.data(), line.data(), _keyword.length()); else return !strncasecmp(_keyword.data(), line.data(), _keyword.length()); } else if (_parms & ANCHOR_RIGHT) { size_t pos = line.length() - _keyword.length(); if (is_case_sensitive()) return !strncmp(_keyword.data(), line.data() + pos, _keyword.length()); else return !strncasecmp(_keyword.data(), line.data() + pos, _keyword.length()); } else { if (is_case_sensitive()) return line.find(_keyword) != string::npos; else { auto it = search(line.begin(), line.end(), _keyword.begin(), _keyword.end(), [](char c1, char c2) { return tolower(c1) == c2; }); return (it != line.end()); } } } // parameters to match, including anchor and whether we search for it // present or missing. refer to public constexpr here for options int _parms; // commited keyword that we are seeking string _keyword; }; #endif // __MATCH__H__ logserver/src/mock_line_provider.h000066400000000000000000000026221512456611200177050ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __MOCK_LINE_PROVIDER__H__ #define __MOCK_LINE_PROVIDER__H__ #include #include "base_line_provider.h" #include "i_log_lines.h" using namespace std; /* Used for testing as a slow source of continual line input */ class MockLineProvider : public BaseLineProvider { public: MockLineProvider(ILogLines* ll, int delay) : BaseLineProvider(ll), _delay(delay) {} virtual ~MockLineProvider() {} protected: virtual void loader() override { while (!ExitSignal::check(false)) { add_line(".............."); if (_delay) { this_thread::sleep_for( chrono::milliseconds(_delay)); } } } // milliseconds between adding new lines int _delay; }; #endif // __MOCK_LINE_PROVIDER__H__ logserver/src/navigation.h000066400000000000000000000156111512456611200161740ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __NAVIGATION__H__ #define __NAVIGATION__H__ #include #include #include #include #include #include #include #include #include #include // can deprecate #include #include "config.h" #include "constants.h" using namespace std; using namespace std::placeholders; /* Stores line number and column offset. Each loglines has its own navigation so * as new ones are pushed they track their own position. */ class Navigation { public: /* constructor sets values to zero */ Navigation() : _cur(0), _tab(0), _max_length(0) {} /* copy constructor used in interface when a new loglines is added with * same dimensions but will be changed promptly if necessary */ Navigation(const Navigation& copy) : _lines(copy._lines), _max_length(0) { _cur.store(copy._cur.load()); _tab.store(copy._tab.load()); } virtual ~Navigation() { } /* process a keystroke binding it to the appropriate function */ virtual bool process(int ch) { static map> functions; if (functions.empty()) { functions[KEY_UP] = bind(&Navigation::up, _1); functions[KEY_DOWN] = bind(&Navigation::down, _1); functions[KEY_LEFT] = bind(&Navigation::left, _1); functions[KEY_RIGHT] = bind(&Navigation::right, _1); functions[KEY_PPAGE] = bind(&Navigation::page_up, _1); functions[KEY_NPAGE] = bind(&Navigation::page_down, _1); functions[KEY_HOME] = bind(&Navigation::start, _1); functions[KEY_END] = bind(&Navigation::end, _1); functions[KEY_SHOME] = bind(&Navigation::line_start, _1); functions[KEY_SEND] = bind(&Navigation::line_end, _1); } if (!functions.count(ch)) return false; functions.at(ch)(this); return true; } virtual void set_cur_length(size_t length) { _max_length = length; } virtual void set_view(const vector& lines) { unique_lock ul(_m); assert(lines.size()); _lines = lines; } /* puts cursor at a line end position, past the last line and accepting * streaming data */ virtual bool at_end() const { return _cur == G::NO_POS; } /* put horizontal shift to leftmost */ virtual void line_start() { _tab = 0; } /* returns true if at leftmost column */ virtual bool at_line_start() const { return _tab == 0; } /* puts line at rightmost based on the max length we have set */ virtual void line_end() { _tab = (_max_length / G::h_shift()) * G::h_shift() - 2 * G::h_shift(); if (_tab > _max_length) _tab = 0; } /* go to top line */ virtual void start() { set(0); } /* go to tail (following) line past the last one */ virtual void end() { set(G::NO_POS); } /* slide horizontal shift left */ virtual void left() { if (_tab >= G::h_shift()) _tab -= G::h_shift(); else _tab = 0; } /* slide horizontal shift right */ virtual void right() { _tab += G::h_shift(); } /* jumps to a specific line number. if save_jump is true then the * current line and the destination are linked so they can be jumped * between then later */ virtual void goto_line(size_t line, bool save_jump) { unique_lock ul(_m); if (save_jump) { _jumps[line] = _cur; _jumps[_cur] = line; } set(line); } /* if there is a backjump marker then follow it back to the source */ virtual void jump_back() { unique_lock ul(_m); if (_jumps.count(_cur)) { set(_jumps[_cur]); } } /* Sets the column to the lower bound multiple of h_shift() for pos */ virtual void goto_pos(size_t pos) { if (pos == string::npos) return; _tab = pos - (pos % G::h_shift()); } /* returns current horizontal offset position */ virtual size_t tab() const { return _tab; } // up arrow pressed virtual void up() { unique_lock ul(_m); if (_lines.empty()) return; set(safe_up(midpos() - 1)); } // down arrow pressed virtual void down() { unique_lock ul(_m); if (_lines.empty()) return; if (_cur != _lines[midpos()]) { set(safe_down(midpos())); } else { set(safe_down(midpos() + 1)); } } // pageup pressed virtual void page_up() { unique_lock ul(_m); if (_lines.empty()) return; set(safe_up(0)); } // pagedown pressed virtual void page_down() { unique_lock ul(_m); if (_lines.empty()) return; set(safe_down(_lines.size() - 1)); } // returns the current line number virtual size_t cur() const { return _cur; } // trace the stored data virtual inline void trace(ostream& out) { unique_lock ul(_m); trace_locked(out); } protected: // implementation of tracing with lock held virtual inline void trace_locked(ostream& out) { out << "(navi) cur=" << _cur << " lines=" << _lines.size() << endl; } /* look for a move we can make upwards. start to the first non-NO_POS * value in _lines starting at pos and looking downwards in _lines */ virtual size_t safe_up(size_t pos) { while (safe(pos) && _lines[pos] == G::NO_POS) ++pos; if (!safe(pos)) return -1; return _lines[pos]; } /* look for a move we can make downwards. start at the first non-NO_POS * value in _lines starting at pos and looking upwards in _lines */ virtual size_t safe_down(size_t pos) const { if (pos == G::NO_POS) return _lines.size() - 1; while (safe(pos) && _lines[pos] == G::NO_POS) --pos; if (!safe(pos)) return -1; return _lines[pos]; } /* check that array access at pos in _lines will be okay */ virtual bool safe(size_t pos) const { return (pos < _lines.size()); } /* returns the middle of the _lines vector, indicating the line in the * centre of the screen */ virtual size_t midpos() const { if (_lines.size() == 0) return 0; return (_lines.size() - 1) / 2; } /* move the line number to this position */ virtual void set(size_t pos) { assert(pos < G::EOF_POS || pos == G::NO_POS); _cur = pos; } /* viewport from the filter runner for navigation */ vector _lines; /* current vertical offset */ atomic _cur; /* current horizontal offset */ atomic _tab; /* max line length that we have seen to manage horizontal moving */ atomic _max_length; /* for thread safety */ mutable mutex _m; /* map of positios that we have set jumps points between */ map _jumps; }; #endif // __NAVIGATION__H__ logserver/src/pin_manager.h000066400000000000000000000101601512456611200163070ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __PIN_MANAGER__H__ #define __PIN_MANAGER__H__ #include #include #include "constants.h" using namespace std; /* PinManager stores the lines that have been pinned down. Updates positions * during insertions. Returns nearest pin relative to a whence and direction. */ class PinManager { public: /* default constructor */ PinManager() {} /* default destructor */ virtual ~PinManager() {} // Given a position pos and a direction dir, and the pair [pos, range] // corresponding to the result of a keyword search, pinsider considers // pinned lines and whether it should interfere. It either returns the // result.pos value it was going to display, or the position of the // first pinned line that is between pos < pin < result.first virtual size_t pinsider(size_t pos, int dir, pair result) { shared_lock ul(_m); optional pin = nearest_pin(pos, dir); size_t match = result.first; if (!pin) return match; size_t range = result.second; // we have a pin before the result, return pin if (dir == G::DIR_DOWN && match != G::NO_POS && *pin < match) return *pin; if (dir == G::DIR_UP && match != G::NO_POS && *pin > match) return *pin; // we have no result and we have maximal range so no result will // come, show the pin. if (match == G::NO_POS && range == G::NO_POS) return *pin; // we have no match within an explored range, but we do have a // pin within that range return the pin if (dir == G::DIR_DOWN && match == G::NO_POS && *pin < range) return *pin; if (dir == G::DIR_UP && match == G::NO_POS && *pin > range) return *pin; // no relevant pin. return top if there was no match and we're // moving up, otherwise return the match (or go to end) if (dir == G::DIR_UP && match == G::NO_POS) return 0; return match; } // new lines have been inserted. adjust the pins appropriately virtual void insertion(size_t pos, size_t amount) { unique_lock ul(_m); set result; for (const auto& x : _pins) { if (x < pos) result.insert(result.end(), x); else result.insert(result.end(), x + amount); } _pins.swap(result); } // clear out the lines because log lines is cleared virtual void clear() { unique_lock ul(_m); _pins.clear(); } // adds a new pinned line virtual void set_pin(size_t pos) { assert(pos != G::NO_POS); unique_lock ul(_m); _pins.insert(pos); } // toggles whether a line is pinned virtual void toggle_pin(size_t pos) { assert(pos != G::NO_POS); unique_lock ul(_m); if (_pins.count(pos)) _pins.erase(pos); else _pins.insert(pos); } // returns true if the line is pinned virtual bool is_pinned(size_t pos) { shared_lock ul(_m); return _pins.count(pos); } // returns nearest pin starting from parameter whence position and // moving in parameter dir direction. Returns nullopt if no pin is found // in that direction virtual optional nearest_pin(size_t whence, int dir) { shared_lock ul(_m); if (dir == G::DIR_DOWN) { // search down auto it = _pins.lower_bound(whence); if (it == _pins.end()) return nullopt; return *it; } else { assert(dir == G::DIR_UP); // search up auto it = _pins.lower_bound(whence); if (it == _pins.begin()) return nullopt; --it; return *it; } } protected: set _pins; // thread safety shared_mutex _m; }; #endif // __PIN_MANAGER__H__ logserver/src/render_parms.h000066400000000000000000000015421512456611200165140ustar00rootroot00000000000000#ifndef __RENDER_PARMS__H__ #define __RENDER_PARMS__H__ #include #include #include using namespace std; class LineFilterKeyword; class PinManager; /* structure storing the current values of a set of render parms at the time of * rendering in case they change during the process */ struct RenderParms { // renderer parameters bool colour; bool line_numbers_off; size_t cols; size_t radius; // epoch parameters size_t cur_epoch; // filter manager parameters size_t context_length; int filter_keywords; vector keywords; PinManager* pins; // loglines parameters size_t total_length; // character to use as tab optional tab_key; set suppressed_tabs; TabData* tab_data; // logline pointer LogLines* ll; // navigation position Navigation* navi; }; #endif // __RENDER_PARMS__H__ logserver/src/render_queue.h000066400000000000000000000070651512456611200165240ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __RENDER_QUEUE__H__ #define __RENDER_QUEUE__H__ #include #include #include #include #include "constants.h" #include "format_string.h" /* Render queue stores the lines of logserver to draw. Each loglines is * associated with its own render queue. The filter runner creates FormatStrings * at a position and passes it to the render queue. The Renderer itself pulls * FormatStrings from the queue and actually draws them. The current epoch for * the rendered values is passed alongside when added and the requested epoch is * provided when removing. RenderQueue takes ownership of the FormatStrings */ class RenderQueue { public: RenderQueue() {} /* Goes through the queue and deletes any format strings that weren't * rendered already */ virtual ~RenderQueue() { unique_lock ul(_m); while (_queue.size()) { auto ret = _queue.front(); delete get<1>(ret); _queue.pop_front(); } _cond.notify_all(); } /* add a format string at position pos for epoch render_epoch. This * function return falses if we aren't interested in this epoch */ bool add(FormatString* data, size_t pos, size_t render_epoch) { unique_lock ul(_m); assert(render_epoch != G::NO_POS); if (_highest_epoch && render_epoch < *_highest_epoch) { delete data; return false; } _queue.push_back(make_tuple(pos, data, render_epoch)); _cond.notify_all(); return true; } // removes a FormatString from the queue with a target epoch. // pos and fs are set if one is found, and true is returned. // any FormatStrings for earlier epochs are deleted bool remove(size_t render_epoch, size_t* pos, FormatString** fs) { unique_lock ul(_m); if (!_highest_epoch || *_highest_epoch < render_epoch) _highest_epoch = render_epoch; // we can pop and push through this iteration so we note the // queue size to make sure we only pass through it once size_t n = _queue.size(); while (n) { assert(_queue.size()); auto ret = _queue.front(); _queue.pop_front(); if (get<2>(ret) < render_epoch) { delete get<1>(ret); } else if (get<2>(ret) == render_epoch) { *pos = get<0>(ret); *fs = get<1>(ret); return true; } else { _queue.push_back(ret); } --n; } return false; } // waits briefly for new lines to appear in queue. void wait_for_work() { unique_lock ul(_m); if (!_queue.empty()) return; _cond.wait_for(ul, chrono::milliseconds(100)); } // returns true if the queue is empty bool empty() { unique_lock ul(_m); return _queue.empty(); } protected: // list of (pos, string, epoch) tuples list> _queue; // mutex for thread safety mutex _m; // signaled when new lines are added to queue condition_variable _cond; // highest requested epoch optional _highest_epoch; }; #endif // __RENDER_QUEUE__H__ logserver/src/renderer.h000066400000000000000000000250241512456611200156420ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __RENDERER__H__ #define __RENDERER__H__ #include #include #include "colour.h" #include "constants.h" #include "curses_wrapper.h" #include "epoch.h" #include "format_string.h" #include "render_parms.h" #include "render_queue.h" typedef function TLineCB; /* Renderer is the class that takes lines to draw and actually ncurses them onto * the screen. It has a RenderQueue object where lines that are computed from * filterrunner are put into. It tracks the epoch and extracts and draws only * lines corresponding to the current epoch. It also has two callback functions, * _title_bar and _status_bar, which are set at contruction, and use the * interface's context to set correctly. */ class Renderer { public: /* takes callbacks to get data for title and status bars and the epoch * instance. gets the screen size and setups the geometry. */ Renderer(const TLineCB& title_bar, const TLineCB& status_bar, Epoch* epoch) : _line_number_off(false), _epoch(epoch), _resting_y(0), _resting_x(0), _cur_epoch(0), _colour(true), _title_bar(title_bar), _status_bar(status_bar) { unique_lock ul(_m); _rows = 0; _cols = 0; setup_screen(); } /* joins threads and ends ncurses */ virtual ~Renderer() { _renderer_thread->join(); _redrawer_thread->join(); CursesWrapper::end_win(); } // starts the renderer and the redrawer. renderer gets computed lines // from the filter runner and puts them in the right position. These can // come continually as there may be ongoing searches for keyword matches // that havn't completed. The redrawer on epoch changes draws the title // and status bars virtual void start() { _renderer_thread.reset(new thread(&Renderer::renderer, this)); _redrawer_thread.reset(new thread(&Renderer::redrawer, this)); } /* instance of the render queue to insert lines for the renderer */ virtual RenderQueue* render_queue() { return &_rq; } /* setup the screen and the colours */ virtual void setup_screen() { // get curses lock and call init functions unique_lock ul(CursesWrapper::get_mutex()); set_escdelay(25); initscr(); Colour::init_curses_colours(); keypad(stdscr, true); noecho(); cbreak(); setup_screensize(); } /* returns the radius, which is the number of lines above and below the * centre bar but not the status and title bars */ virtual size_t radius() const { return _radius; } /* turn on and off colouring of lines */ virtual void colour_toggle() { _colour = !_colour; } /* turn on and off rendering of line numbers */ virtual void line_numbers_toggle() { _line_number_off = !_line_number_off; } /* returns whether line numbers are not rendered */ virtual bool line_numbers_off() const { return _line_number_off; } /* returns the width of the screen */ virtual size_t cols() const { unique_lock ul(_m); return _cols; } /* fills out the RenderParms struct with the relevant information in * this class */ virtual void set_render_parms(RenderParms* rp) { rp->colour = _colour; rp->line_numbers_off = line_numbers_off(); rp->cols = cols(); rp->radius = radius(); } protected: /* uses ioctl to get the scree size. sets up the geometry based on that. * resting_xy is where we keep the cursor. * */ virtual void setup_screensize() { struct winsize size; if (ioctl(0, TIOCGWINSZ, (char*) &size) < 0) { // error } else if (_cols != size.ws_col || _rows != size.ws_row) { _cols = size.ws_col; _rows = size.ws_row; G::h_shift(_cols / 2); compute_geometry(); _resting_y = 0; _resting_x = 0; } } // computes radius based on screen size and where the status line goes virtual void compute_geometry() { size_t half = _rows - 5; if (half % 2) --half; _radius = half >> 1; _status_line = 2 * _radius + 3; } /* renderer draws lines that take some time to compute * e.g., because a search on a huge file is not completed * new lines appear in the render queue. */ virtual void renderer() { while (!ExitSignal::check(false)) [[likely]] { // wait until there is a line we may want to render while (_rq.empty()) { _rq.wait_for_work(); if (ExitSignal::check(false)) [[unlikely]] return; } unique_lock ul(_m); // extract the line from the render queue size_t pos = G::NO_POS; FormatString* fs = nullptr; size_t i = 0; while (_rq.remove(_cur_epoch, &pos, &fs)) { // we got a line for this epoch. draw it draw(pos + 2, 0, *fs); delete fs; ++i; } // if we actually rendered something, refresh if (i) CursesWrapper::refresh(); } } /* redrawer draws title and status. It waits on an epoch change, and * then triggers a redrawing */ virtual void redrawer() { while (!ExitSignal::check(false)) [[likely]] { unique_lock ul(_m); // note the current epoch, if its still the same when // we finish wait for it to advance _cur_epoch = _epoch->cur(); // check for screen change setup_screensize(); // unlock and get data from interface ul.unlock(); // runs the callbacks for status and title FormatString title; FormatString status; _title_bar(&title, _cols); _status_bar(&status, _cols); // lock and draw ul.lock(); draw(0, 0, title); _resting_x = 0; _resting_y = 0; draw(_status_line + 1, 0, status); // gets where we left the cursor on the status bar. if // we draw something else we want to go back to this // position CursesWrapper::get_yx(&_resting_y, &_resting_x); CursesWrapper::refresh(); // wait until epoch advances past the valid it was when // we started the render while (!ExitSignal::check(false) && _cur_epoch == _epoch->cur()) { _epoch->wait(&ul); } } } static optional move_to_number(const string& in) { size_t i = 0; while (i < in.size()) { if (in[i] >= '0' && in[i] <= '9') return in.substr(i); ++i; } return nullopt; } size_t parse_ansi(const FormatString& data, size_t pos) const { assert(data[pos] == 0x1b); if (pos + 2 >= data.size()) return pos; if (data[pos + 1] != '[') return pos; size_t end = data.find('m', pos); if (end == string::npos) return pos; optional format = move_to_number( data.substr(pos, end - pos)); size_t subpos; while (format) { try { int val = stoi(*format, &subpos); int attr = 0; bool mode = false; if (val == 4) { attr |= A_UNDERLINE; mode = true; } else if (val == 2) { attr |= A_BOLD; mode = true; } else if (val == 24) { attr |= A_UNDERLINE; mode = false; } else if (val == 22) { attr |= A_BOLD; mode = false; } else if (val == 0) { attr = A_UNDERLINE | A_BOLD; mode = false; } CursesWrapper::attr(attr, mode); format = move_to_number(format->substr(subpos)); // handle } catch (...) { break; } } return end; } /* draws the FormatString data to the screen at postions in the * parameters y and x. */ virtual void draw(size_t y, size_t x, const FormatString& data) const { CursesWrapper::move_yx(y, x); size_t screen_i = 0; for (size_t i = 0; i < data.length(); ++i, ++screen_i) { if (screen_i >= _cols) break; int code = data.code(i); char c = data.at(i); if (_colour) { int attr = COLOR_PAIR(code % G::LOWEST_MODIFIER); if (code & G::BOLD) attr |= A_BOLD; if (code & G::UNDERLINE) attr |= A_UNDERLINE; CursesWrapper::attr(attr, true); } // replace non tab whitespace with space if (c == '\f' || c == '\n' || c == '\r') c = ' '; // implement tab by clearing until 8-aligned if (c == '\t') { for (size_t clearpos = 0; clearpos < 8 - (screen_i % 8); ++clearpos) { CursesWrapper::add_ch(y, screen_i + clearpos, ' '); } screen_i += 8 - (screen_i % 8); } // suppress unprintable characters if (!isprint(c)) c = ' '; // actually draw the char CursesWrapper::add_ch(y, screen_i, c); // if we turned on an attr, turn it off if (_colour) { int attr = COLOR_PAIR(code % G::LOWEST_MODIFIER); if (code & G::BOLD) attr |= A_BOLD; if (code & G::UNDERLINE) attr |= A_UNDERLINE; CursesWrapper::attr(attr, false); } } // clear the rest of the line if (screen_i < _cols) CursesWrapper::clear_eol(); // put the cursor back to where we want it if (_resting_y || _resting_x) CursesWrapper::move_yx(_resting_y, _resting_x); } // the render queue for computed lines as they arrive RenderQueue _rq; // thread safety mutable mutex _m; // thread that draws lines as they are computed unique_ptr _renderer_thread; // thread that draws the title and status bar on epoch change unique_ptr _redrawer_thread; // current radius, meaning lines above centreline we display atomic _radius; // the position of the status line size_t _status_line; // whether line numbers should be rendered atomic _line_number_off; // pointer to current epoch Epoch *_epoch; // screen size in rows size_t _rows; // screen size in columns size_t _cols; // cursor x-y position at end of statusbar so we can return it size_t _resting_y; size_t _resting_x; // the current epoch we are drawing. set by the redrawer thread when it // starts, and used to know when to trigger a redraw and for the // renderer thread to only accept lines for this epoch atomic _cur_epoch; // whether we should have colour in our display atomic _colour; // callback for rendering the title bar TLineCB _title_bar; // callback for rendering the status bar TLineCB _status_bar; }; #endif // __RENDERER__H__ logserver/src/run.h000066400000000000000000000231431512456611200146400ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __RUN__H__ #define __RUN__H__ #include #include #include #include #include #include #include #include #include #include #include #include "tokenizer.h" using namespace std; /* Wrapper around running a command. Takes a |-separated sequence of commands * and runs them in order, feeding the output as input as would be expected. * Allows the result to be read back in at the end */ class Run { /* stores a pair of FDs for a pipe and allows closing either side */ struct PipePair { PipePair() { int r = pipe(&read_end); if (r) { read_end = 0; write_end = 0; } } ~PipePair() { close(); } void close() { if (read_end) ::close(read_end); if (write_end) ::close(write_end); read_end = 0; write_end = 0; } void set_write() { ::close(read_end); read_end = 0; } void set_read() { ::close(write_end); write_end = 0; } int read_end; int write_end; /* casting to int returns the read_end */ operator int*() { return (int*) this; } }; public: Run() : Run("", "") {} explicit Run(const string& cmd) : Run(cmd, "") {} /* constructor takes command and input to use for stdin. it separates * the |-separated commands and assembles the pipeline */ Run(const string& cmd, const string& input) : _cmd(cmd), _started(false), _read(false), _done(false), _status(0) { vector pipeline; _pipes.push_back(nullptr); _pipes.back().reset(new PipePair()); Tokenizer::split_mind_quote(_cmd, "|", &pipeline); for (const auto& x: pipeline) { connect(x); } write_input(input); safety(); } virtual ~Run() {} virtual string command() const { return _cmd; } /* function invoke operator will execute the pipeline */ void operator()() { _started = true; size_t pos = 0; while (pos != _argvs.size()) { _pids.push_back(execute(pos)); if (pos == 0) _pipes[0]->set_write(); ++pos; } _pipes.back()->set_read(); } /* get the return value of the last command in the pipeline */ int result() { if (!_read) read(); return _status; } /* TODO: move read and redirect to a single stream based runner */ int redirect(const string &filename) { ofstream fout(filename); const size_t SIZE = 4096; assert(_pipes.back()->read_end); int r = 0; size_t pid_pos = 0; char buf[SIZE]; while (true) { r = ::read(_pipes.back()->read_end, buf, SIZE); if (r == 0 || (r < 0 && errno == EAGAIN)) { if (pid_pos == _pids.size()) { break; } waitpid(_pids[pid_pos++], &_status, 0); continue; } if (r < 0) { return -1; } fout.write(buf, r); } return 0; } /* read output as a vector */ void read(vector* out) { stringstream ss; ss << read(1000); string s; while (getline(ss, s)) { out->push_back(s); } } /* returns stdout of last process */ string read() { return read(1000); } /* runs through the pipeline closing stdin and waiting for the process * to finish */ void finish() { size_t pid_pos = 0; while (pid_pos < _pids.size()) { _pipes[pid_pos]->close(); waitpid(_pids[pid_pos++], &_status, 0); } } /* read stdout of last process with a timeout */ string read(int runtime) { const size_t SIZE = 4096; stringstream ss; char buf[SIZE]; size_t pid_pos = 0; thread kill_thread(bind(&Run::abort_run, this, runtime)); int r = 0; prepare_read(); while (true) { r = ::read(_pipes.back()->read_end, buf, SIZE); if (r == 0 || (r < 0 && errno == EAGAIN)) { if (pid_pos == _pids.size()) { break; } waitpid(_pids[pid_pos++], &_status, 0); continue; } if (r < 0) { _done = true; kill_thread.join(); return ""; } ss.write(buf, r); } _done = true; kill_thread.join(); return ss.str(); } /* gets the FD for the last command's stdout */ int read_fd() { prepare_read(); return _pipes.back()->read_end; } /* gets the FD for the first command's stdin */ int write_fd() { return _pipes.front()->write_end; } /* closes the write end of the pipelien */ void close_write() { size_t pos = 0; while (pos + 1 < _pipes.size()) { _pipes[pos]->close(); ++pos; } } protected: /* adds a new parameter cmd as a command to the end of the current * pipeline */ void connect(const string_view& cmd) { _argvs.push_back(vector()); _pipes.push_back(nullptr); _pipes.back().reset(new PipePair()); Tokenizer::split_mind_quote_and_copy(cmd, " ", &_argvs.back()); } // TODO: make this a config file /* idea here is to preven running commands that may do real things on * the FS like rm */ static inline const set ALLOWED = { "cat", "sort", "uniq", "ls", "grep", "cut", "tr", "sed", "awk", "csv_mr", "fgrep", "/bin/which", "whoami", "base64", "echo", "file", "packetgrep", "wc", "xsel", "mplayer" }; /* checks all commands are on the allow list and throws if not */ void safety() { for (const auto &x : _argvs) { if (!ALLOWED.count(string(x[0]))) { _argvs.clear(); _pipes.clear(); throw logic_error(_cmd + " not allowed"); } } } /* runs the pipeline command at parameter pos's position */ pid_t execute(size_t pos) { const char &c = _argvs[pos][0][0]; if (c != '/' && c != '~' && c != '.') { string which_path; char *path_env = getenv("PATH"); if (!path_env) return 0; char *delete_path = strdup(path_env); if (!delete_path) return 0; char *dir = strtok(delete_path, ":"); while (dir) { which_path = string(dir) + "/" + _argvs[pos][0]; if (access(which_path.c_str(), X_OK) == 0) { break; } dir = strtok(NULL, ":"); } free(delete_path); if (which_path.empty()) throw logic_error(string("fork(): failed")); _argvs[pos][0] = which_path; } pid_t pid = fork(); if (pid == -1) { throw logic_error(string("fork(): failed")); } if (pid == 0) { // I am child int r = 0; for (size_t i = 0; i < _pipes.size(); ++i) { if (i == pos || i == pos+1) continue; _pipes[i]->close(); } _pipes[pos]->set_read(); _pipes[pos+1]->set_write(); int keep_read = _pipes[pos]->read_end; int keep_write = _pipes[pos + 1]->write_end; int fdlimit = (int)sysconf(_SC_OPEN_MAX); for (int i = 3; i < fdlimit; i++) { if (i == keep_read || i == keep_write) continue; close(i); } char** argv = get_c_args(pos); r = dup2(_pipes[pos]->read_end, STDIN_FILENO); if (r == -1) { exit(-1); } close(STDERR_FILENO); // TODO: decide what to do with STDERR_FILENO r = dup2(_pipes[pos + 1]->write_end, STDOUT_FILENO); if (r == -1) { exit(-1); } execv(argv[0], (char * const *) argv); _pipes[pos]->close(); _pipes[pos + 1]->close(); char** p = argv; while (!*p++) delete[](*p); delete[] argv; exit(-1); } return pid; } /* used to separate check if command is still running and after a * timeout will kill the processes */ void abort_run(int when) { struct timeval tv; gettimeofday(&tv, nullptr); int now = tv.tv_sec; while (!_done && (now + when > tv.tv_sec)) { usleep(1000); gettimeofday(&tv, nullptr); } if (_done) return; for (auto &x : _pids) { kill(x, SIGKILL); } } void prepare_read() { _read = true; assert(_pipes.size()); assert(_pids.size()); assert(_pipes.back()->read_end); _done = false; } /* writes data to the front of the pipeline */ void write_input(const string& data) { size_t todo = data.length(); while (todo) { int r = ::write(_pipes.front()->write_end, data.c_str(), data.length()); if (r < 0) throw logic_error(string("write(): failed.")); size_t written = (size_t) r; if (written == 0) throw logic_error(string("write(): failed.")); todo -= written; } } /* converts the pipeline command at parameter pos's position into a * argv-style char** for running */ char** get_c_args(int pos) { char** retval = new char*[_argvs[pos].size() + 1]; for (size_t i = 0; i < _argvs[pos].size(); ++i) { retval[i] = new char[_argvs[pos][i].length() + 1]; string arg = _argvs[pos][i]; assert(arg.size()); if (arg.size() > 2 && arg[0] == '"' && arg[arg.size() - 1] == '"') { strncpy(retval[i], arg.data() + 1, arg.size() - 2); retval[i][arg.size() - 2] = '\0'; } else { strncpy(retval[i], arg.data(), arg.size()); retval[i][arg.size()] = '\0'; } } retval[_argvs[pos].size()] = nullptr; return retval; } /* vector of each command, consisting of vector of args */ vector> _argvs; /* pipes connecting the processes */ vector> _pipes; /* pids for the processes */ vector _pids; /* the command we are given */ string _cmd; /* has execution started */ bool _started; /* has reading happened */ bool _read; /* is execution done */ atomic _done; /* what is return val of last command */ int _status; }; #endif // __RUN__H__ logserver/src/search_range.h000066400000000000000000000111501512456611200164500ustar00rootroot00000000000000#ifndef __SEARCH_RANGE__H__ #define __SEARCH_RANGE__H__ #include #include "line_filter_keyword.h" #include "log_lines.h" using namespace std; /* Helper class with static functions used to search multiple keywords for ranges */ class SearchRange { public: // returns the furthest position starting from pos in direction dir such // that all keywords have been search until the range. T is either // LineFilterKeyword* or unique_ptr template static size_t junction_range(size_t pos, bool dir, const vector& keywords, size_t length) { static_assert( is_same_v, LineFilterKeyword*> || is_same_v, unique_ptr>, "T must be LineFilterKeyword* or unique_ptr" ); size_t ret = G::NO_POS; // search all keywords, get their searched range, and return the // minimum, meaning all have been searched to that point for (auto &x : keywords) { size_t range; if (x->empty()) { if (!dir) range = 0; else range = length - 1; } else { range = x->next_range(pos, dir); } if (dir && (ret == G::NO_POS || (range != G::NO_POS && range < ret))) ret = range; if (!dir && (ret == G::NO_POS || (range != G::NO_POS && range > ret))) ret = range; } return ret; } /* returns a pair of position and range corresponding to the position of * the next keyword starting for pos and searching in direction dir. The * range indicates how far it was able to search. Returns G::NO_POS if * not found within the valid search range. T is either * LineFilterKeyword* or unique_ptr. */ template static pair keyword_disjunction( size_t pos, bool dir, const vector& keywords, size_t total_length) { size_t range = junction_range( pos, dir, keywords, total_length); size_t ret = G::NO_POS; if (range == G::NO_POS) return make_pair(G::NO_POS, G::NO_POS); if (pos == G::NO_POS) return make_pair(G::NO_POS, G::NO_POS); if (dir && pos > range) return make_pair(G::NO_POS, G::NO_POS); for (auto &x : keywords) { if (x->empty()) continue; size_t match = x->next_match(pos, dir); if (dir && (ret == G::NO_POS || (match != G::NO_POS && match < ret))) ret = match; if (!dir && (ret == G::NO_POS || (match != G::NO_POS && match > ret))) ret = match; } return make_pair(ret, range); } /* returns a pair of position and range corresponding to the position of * the next line that has all keywords matching starting for pos and * searching in direction dir. The range indicates how far it was able * to search. Returns G::NO_POS if not found within the valid search * range. T is either LineFilterKeyword* or * unique_ptr. */ template static pair keyword_conjunction( size_t pos, bool dir, const vector& keywords, size_t total_length) { size_t range = junction_range( pos, dir, keywords, total_length); size_t ret = pos - !dir; if (range == G::NO_POS || ret == G::NO_POS || pos == G::NO_POS) return make_pair(G::NO_POS, G::NO_POS); if (dir && pos > range) return make_pair(G::NO_POS, G::NO_POS); bool dirty = true; while (dirty) { dirty = false; for (auto &x : keywords) { if (x->empty()) continue; if (x->is_match(ret)) continue; size_t match = x->next_match(ret, dir); if (match == G::NO_POS) return make_pair(G::NO_POS, G::NO_POS); if (dir && match > range) return make_pair(G::NO_POS, G::NO_POS); if (!dir && match < range) return make_pair(G::NO_POS, G::NO_POS); if (ret != match) { dirty = true; ret = match; } } } return make_pair(ret, range); } }; #endif // __SEARCH_RANGE__H__ logserver/src/static_demo.h000066400000000000000000000141471512456611200163330ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __STATIC_DEMO__H__ #define __STATIC_DEMO__H__ #include #include #include #include "version.h" using namespace std; /* Another static help-like page that shows some of the features of logserver */ class StaticDemo { public: static inline const string NAME = "demo"; static void render(ILogLines* out) { for (const auto& x : demo_lines) out->add_line(x); } protected: static inline const vector demo_lines = { "Look! This is a long line below. Hit 'b' to break it up:", "", "Shem was a sham and a low sham and his lowness creeped out first via foodstuffs. So low was he that he preferred Gibsen's teatime salmon tinned, as inexpensive as pleasing, to the plumpest roeheavy lax or the friskiest parr or smolt troutlet that ever was gaffed between Leixlip and Island Bridge and many was the time he repeated in his botulism that no junglegrown pineapple ever smacked like the whoppers you shook out of Ananias' cans, Findlater and Gladstone's, Corner House, Englend. None of your inchthick blueblooded Balaclava fried-at-belief-stakes or juicejelly legs of the Grex's molten mutton or greasilygristly grunters' goupons or slice upon slab of luscious goosebosom with lump after load of plumpudding stuffing all aswim in a swamp of bogoakgravy for that greekenhearted yude! Rosbif of Old Zealand! he could not attouch it. See what happens when your somatophage merman takes his fancy to our virgitarian swan? He even ran away with hunself and became a farsoonerite, saying he would far sooner muddle through the hash of lentils in Europe than meddle with Irrland's split little pea. Once when among those rebels in a state of hopelessly helpless intoxication the piscivore strove to lift a czitround peel to either nostril, hiccupping, apparently impromptued by the hibat he had with his glottal stop, that he kukkakould flowrish for ever by the smell, as the czitr, as the kcedron, like a scedar, of the founts, on mountains, with limon on, of Lebanon. O! the lowness of him was beneath all up to that sunk to! No likedbylike firewater or firstserved firstshot or gulletburn gin or honest brewbarrett beer either. (notice after breaking it doesn't cut sharply mid word?)", "", "but if there is no decent line breaks within a reasonable range it just does it sharply", "aaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", "", "if it notices there is a frequently occuring possible line break signature it uses that: either \\n or |", "hello\\nthere\\nlook\\nat\\nall\\nthese\\nlines!", "hello|there|look|at|all|these|lines!", "", "if you want to break on your own choice of symbol when there it isn't automatic", "then use B followed by the character you want to break on, even a letter. you can do two tries.", "try.it.out.here,and,see.what,happens", "try.it.out.here,and,see.what,happens", "", "if it looks like an http query string it just splits it on the ampersand", "lang=en&event=pageview&referer=someserver.com&width=780&height=1080&battery=empty&sun=shining&birds=chirping", "", "breaking also does JSON too nicely, give it a try", "{\"a\":{\"b\":[1,2,3], \"c\":{\"d\":\"hello\", \"e\":\"there\"}, \"f\":15}, \"g\":{}}", "", "", "you know what time it is? 1702154596", "(use 'i' to turn the time stamp into human time", "it also works in millis 1702154596123", "", "", "ooo this looks like base64, hit 'i' to see what it is!", "aGVsbG8gdGhlcmUhCg==", "", "this one is base64 wrapping base64 wrapping base64 . keep trying!", "WVZoUmJtTjVRbXRpTWpWc1EyYzlQUW89Cg==" "", "hexencoding: referrer=https%3A%2F%2Fpotatocrunchcereal.com%2F", "", "you can select a subset of lines. hit ! on this line to add a divider, then go DOWN TO SIGNAL", "", "maybe these", "lines are", "interesting to", "look at", "in isolation", "use ESC to return", "", "SIGNAL! hit ! again, and then go into the middle part and hit '%' to take a subset", "these ! lines will also stick around during filter mode", "", "this line is special. let's mark it with J (capital)", "there should be a * next to the number now. hit 'n' to hide numbers and special marker", "move down to here and hit lower case j to jump back", "", "you can have many special lines with J and use j to jump among them.", "special lines can be made unspecial also with J, it toggles" "", "you can also jump directly to a line by number", "hit : and type a number", "behold!" "", "", "things before a colon are signalled as they may be keys in various formats", "httpheader_key: httpheader_value", "otherheader: othervalue", "{", " \"jsonkey\": \"jsonvalue\",", " \"otherkey\": \"other value\"", "}", }; }; #endif // __STATIC_DEMO__H__ logserver/src/static_help.h000066400000000000000000000307611512456611200163370ustar00rootroot00000000000000#ifndef __STATIC_HELP__H__ #define __STATIC_HELP__H__ #include #include #include #include "version.h" using namespace std; /* This class stores the help screen for logserver. It takes an ILogLines and * populates it with all the help lines */ class StaticHelp { public: static inline const string NAME = "help"; static void render(ILogLines* out) { Run run("whoami"); run(); string me = run.read(); if (!me.empty()) me = me.substr(0, me.length() - 1); out->add_line(help_lines[0]); string top = " "; string name = top + " " + me + "'s logserver"; for (size_t i = 0; i < me.length() + 14; ++i) { top += '='; } out->add_line(top); out->add_line(name); out->add_line(top); out->add_line(""); out->add_line(""); out->add_line(help_lines[6]); out->add_line(""); vector quotebox = help_quote(); size_t i; for (i = 0; i < quotebox.size(); ++i) { out->add_line(help_lines[8 + i] + quotebox[i]); } i += 8; while (i < help_lines.size()) { out->add_line(help_lines[i]); ++i; } } protected: static inline const vector help_lines = { " ", " ================", " YOUR LOGSERVER", " ================", "", "", string(" version: ") + Version::VERSION, "", " ",//+----------------+", " (ESC goes back) +--+ ",//| LOGS ANYONE? |", " | | ",//| LOGS?? |", " | | ",//+----------------+", " --+--+-- / ", " / \\ / ", " | | --/ ", " \\ - / ", " \\_/ ", " (o)==) | | ", " (o)=====) ------------------- ", " (o)========) \\ / ", " ------------- \\ |\\ /| / ", " | \\ |/ \\| / ", " | \\ . / ", " +------------\\ . / ", " \\ . / ", " \\ / ", " \\ / ", " \\ / ", " - ", " / \\ ", " | | ", " bizzz---- \\_/ ", "", "", " Logserver", " Copyright (C) 2017-2025 Joel Reardon", "", " This program is free software: you can redistribute it and/or modify", " it under the terms of the GNU General Public License as published by", " the Free Software Foundation, either version 3 of the License, or", " (at your option) any later version.", "", " This program is distributed in the hope that it will be useful,", " but WITHOUT ANY WARRANTY; without even the implied warranty of", " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the", " GNU General Public License for more details.", "", " You should have received a copy of the GNU General Public License", " along with this program. If not, see .", "", "", "KEYBINDINGS FOR NAVIGATION:", " arrows:", " Move around in the file. Long lines do not wrap so left and right moves accordingly.", "", " home: Move to the top of the file.", "", " end: Move to one-past the end of the file, which will display streaming data", "", " shift+home:", " move left to the start of the line", "", " shift+end:", " move right to the end of the line", "", " page-up:", " move to the top line visible", "", " page-down:", " move to the bottom line visible", "", " colon: accepts a number afterwards, and moves to that line number", "", "", "KEYBINDINGS FOR SEARCHING:", " slash: Accepts a keyword afterwards, and adds it as a search term.", "", " backslash:", " Accepts a keyword afterwards, and adds it as a reverse search.", "", " caret: Accepts a keyword afterwards, and adds it as a starts with search.", "", " dollar sign:", " Accepts a keyword afterwards, and adds it as an ends with search.", "", " tab: Alternates among search modes.", " ALL mode shows all lines and highlights matching keywords.", " OR mode (disjunctive) shows lines that match any keyword.", " AND mode (conjunctive) shows lines that match all keywords.", "", " shift+left:", " move left on the current line to the next matching keyword", "", " shift+right:", " move right on the current line to the next matching keyword", "", " shift+up:", " In ALL mode, moves up to the next line that matches any keyword.", " In OR mode, moves up to the next line that matches", " a keyword that is not matched on the current line.", " Useful for staying in OR mode but skipping large amounts of the same match.", "", " shift+down:", " Same as shift+up but searches downwards.", "", " backspace:", " Removes the most-recently added search term.", "", " plus: Add one more line of context around matching lines.", "", " minus: Remove one line of context around matching lines.", "", "", "KEYBINDINGS FOR LINE OPERATIONS:", " octothorpe:", " Accepts a string afterwards, and adds the current line and surrounding view", " along with that comment to a file in current directory called storytime.txt.", "", " letter e:", " Accepts a string starting with the current line. Changes to that line are", " reflected in the display (but not the original file).", "", " letter b:", " Breaks a long line up and inserts the new lines.", " This uses a number of heuristics in an attempt to be elegant.", " It uses spaces and punctuation to give a ragged-right in text.", " It uses ampersands and equals to infer HTTP query strings and breaks on the ampersand.", " It uses quotes and braces to infer JSON for pretty printing.", " It uses periodic backslash-n to infer escape newlines and breaks on those.", "", " letter B:", " Accepts a single character next, and performs the break functionality", " described for the letter b using that specific character,", " i.e., replaces that character with newlines.", "", " letter i:", " Intelligence for lines. Replaces UNIX timestamps with human time.", " Looks for sequences of base64 or base16 encoded text based on printable characters after decoding.", " Repeated pressing of 'i' softens the heuristics of how much text needs to be printable.", "", " letter d:", " If hit twice in a row, deletes the current line from the display (not from the original file).", "", " asterisk:", " Pins the current line. When searching for keywords pinned lines will appear in OR and AND mode despite not matching.", "", " letter s:", " Accepts a string afterwards, and writes the current line to the file specified by that string.", "", " letter f:", " Follows a link on the current line.", " If logserver is given the output of `grep -n`", " then each line will link to that file and line number and 'f' will follow it.", " If logserver is reading a ctags file, then each line will be a link to that target.", " letter m:", " Merges the next line to the current line.", "", "", "KEYBINDINGS FOR GLOBAL OPERATIONS AND STACKED VIEWS", " letter q:", " Quits logserver.", "", " letter n:", " Toggle line numbers on and off.", "", " letter c:", " Toggle colouring on and off.", "", " letter S:", " Accepts a string afterwards, and writes the entire log file to the file specified by that string.", " This is useful when data is bring streamed into logserver by program output.", "", " letter h:", " Launch the help screen, pushes on the stack.", "", " letter C:", " Clears the entire contents of the log. This is useful when streaming in data,", " e.g., a device log, and you want the logs relevant to a particular event that is about to be triggered.", "", " exclamation:", " Inserts a new pinned dash line at the current position.", " Appends it if the current line is one-past the end.", " Useful for separating segments of the log, such as the debug log corresponding to", " immediately before pressing a button which cause a program crash to mark these events", "", " percent:" " In OR and AND mode, applies the current filter and creates a new view with just the matching lines", " and puts that on the stack (i.e., some percent of the logs).", " In ALL mode, goes up and down from the current position searching for a pinned like,", " such as one created by an exclamation, and pushes a new view on the stack bounded by those", " (or the top and bottom if none are found).", "", " less-than:", " Moves left on the stack of views", "", " greater-than:", " Moves right on the stack of views.", "", " escape:", " Pops the top most (right most) view on the stack. Does nothing if there is only one view.", "", " pipe:", " Accepts a string afterwards and runs that command, passing the current view as stdin,", " and pushing a new view on the stack with the stdout of the command as its contents.", " For security purposes the set of commands that can be run is limited to the following:", " cat, sort, uniq, ls, grep, cut, tr, sed, awk, fgrep, which, whoami, base64, echo, file, wc, xsel, mplayer.", "", "", " letter T:", " Consider the next key pressed as the tab character, so T-comma can be for CSVs", "", " letter t:", " Toggle tab mode, where tab character and column widths are used to align tabular data", "", " numbers 0-9:", " In tab mode, toggle suppression of column number. Columns are indexed starting at 1, with number ten as 0", "", "", "", " Wanna try it out? Hit 'f' on the next line to see!", " @DEMOTIME", "" }; /* choose a random quote for logserver to state */ static vector help_quote() { vector ret; int x = rand() % 7; if (x == 0) { ret.push_back("+----------------+"); ret.push_back("+ LOGS ANYONE? +"); ret.push_back("+ LOGS?? +"); ret.push_back("+----------------+"); } else if (x == 1) { ret.push_back("+------------------+"); ret.push_back("+ STREAMING LOGS +"); ret.push_back("+ AVAILABLE NOW! +"); ret.push_back("+------------------+"); } else if (x == 2) { ret.push_back("+-----------------+"); ret.push_back("+ LOGS HERE, AT +"); ret.push_back("+ YOUR SERVICE! +"); ret.push_back("+-----------------+"); } else if (x == 3) { ret.push_back("+-----------------------+"); ret.push_back("+ PLEASE ALLOW ME TO +"); ret.push_back("+ SERVE YOU SOME LOGS! +"); ret.push_back("+-----------------------+"); } else if (x == 4) { ret.push_back("+-------------------------+"); ret.push_back("+ AT ONCE, RIGHT AWAY +"); ret.push_back("+ MESDAMES ET MESSEURS! +"); ret.push_back("+-------------------------+"); } else if (x == 5) { ret.push_back("+--------------------------+"); ret.push_back("+ REMEMBER, DON'T LOG +"); ret.push_back("+ SENSITIVE INFOMRATION! +"); ret.push_back("+--------------------------+"); } else if (x == 6) { ret.push_back("+----------------------+"); ret.push_back("+ ETERNALLY GRATEFUL +"); ret.push_back("+ TO SERVE YOU LOGS +"); ret.push_back("+----------------------+"); } return ret; } }; #endif // __STATIC_HELP__H__ logserver/src/story.h000066400000000000000000000043301512456611200152110ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __STORY__H__ #define __STORY__H__ #include #include #include #include #include "log_lines.h" #include "navigation.h" using namespace std; /* Story class wraps around an fstream for a file and uses that to write * comments and snippits. Used for doing analysis work and wanting to keep a * record or notes while doing it */ class Story { public: Story() { } virtual ~Story() { } /* user has provided a comment in data for a line in ll at position in * navi. The current viewport as line numbers in view. Creates a * snippet of the current position with nearby context and writes it to * the story file. */ // TODO: make 6 radius configurable virtual void write(const string& data, LogLines* ll, Navigation* navi, const set& view) { if (!_fout.is_open()) open(); size_t pos = navi->cur(); if (navi->at_end()) pos = ll->length(); _fout << "--" << endl << endl; _fout << "@" << ll->file_name() << ":" << pos << endl; _fout << data << endl << endl; stringstream ss; ll->quote(pos, view, 6, &ss); _fout << ss.str() << endl << endl; _fout.flush(); ofstream f_summary("SUMMARY", fstream::out | fstream::app); f_summary << "@" << ll->file_name() << ":" << pos << " " << data << endl; } protected: // TODO: make this file configurable /* Opens the storytime.txt file for appending data from comments */ virtual void open() { _fout.open("storytime.txt", fstream::out | fstream::app); } ofstream _fout; }; #endif // __STORY__H__ logserver/src/tab_data.h000066400000000000000000000031551512456611200155740ustar00rootroot00000000000000#ifndef __TAB_DATA__H__ #define __TAB_DATA__H__ #include #include #include #include #include "tab_data.h" using namespace std; /* stores information about the column widths in tab mode */ class TabData { public: TabData() : _countdown(0) {} /* observe the width of a column */ virtual void observe_col(size_t num, size_t width) { unique_lock ul(_m); if (num >= _sizes.size()) { _sizes.resize(num + 1); } auto vals = _sizes[num]; if (width > vals.first) { vals.second = vals.first; vals.first = width; } else if (width > vals.second) { vals.second = width; } _sizes[num] = vals; } virtual void maybe_evict() { if (_countdown == 0) { for (size_t i = 0; i < _sizes.size(); ++i) { _sizes[i].first = _sizes[i].second; _sizes[i].second = 0; } _countdown = 5; } else { --_countdown; } } /* returns the width afforded to column number num */ virtual size_t width(size_t num) const { unique_lock ul(_m); if (num < _sizes.size()) { const auto& vals = _sizes[num]; // if the longest col is 50% more than the second // longest, just use the second longest if (vals.second && vals.second * 1.5 < vals.first) return vals.second; return vals.first; } return 0; } virtual void clear() { unique_lock ul(_m); _sizes.clear(); } protected: // used for random eviction so long cells that aren't reseen go away size_t _countdown; // column number to the largest and second largest width observed vector> _sizes; // thread safety mutable mutex _m; }; #endif // __TAB_DATA__H__ logserver/src/tokenizer.h000066400000000000000000000712501512456611200160500ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __TOKENIZER__H__ #define __TOKENIZER__H__ #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; class Tokenizer { public: static void printable_segments(const string_view& s, size_t min, vector* out) { assert(out); size_t pos = 0; while (pos < s.length()) { while (pos < s.length() && !(isprint(s[pos]) || isspace(s[pos]))) ++pos; if (pos == s.length()) break; size_t start = pos; while (pos < s.length() && (isprint(s[pos]) || isspace(s[pos]))) ++pos; if (pos - start >= min) out->emplace_back(s.substr(start, pos - start)); } } static string_view get_token(const string_view& s, size_t pos, const string& end) { set terms; size_t cur = pos; for (auto &x : end) terms.insert(x); while (++cur != s.length()) { if (terms.count(s.at(cur))) { return s.substr(pos, cur - pos); } } return s.substr(pos); } static bool match_pairs(const string_view& s, vector* components, vector* matchings) { return match_pairs(s, components, matchings, false); } static bool match_pairs(const string_view& s, vector* components, vector* matchings, bool mind_quote) { assert(matchings); matchings->resize(s.length(), string::npos); components->resize(s.length()); vector> stack; string annotated; if (mind_quote) annotate_quote(s, &annotated); for (size_t i = 0; i < s.length(); ++i) { if (mind_quote && annotated[i] != '_') continue; if (s[i] == '{' || s[i] == '[') { stack.push_back(make_pair(i, s[i])); } if (s[i] == '}' || s[i] == ']') { if (stack.empty()) throw string("parse error: empty stack"); if ((stack.back().second == '{' && s[i] == ']') || (stack.back().second == '[' && s[i] == '}')) { throw string("parse error mismatch {}[]"); } size_t start = stack.back().first; (*matchings)[i] = start; (*matchings)[start] = i; (*components)[start] = s.substr(start, i - start + 1); stack.pop_back(); } } if (stack.size()) throw string("parse error: end with stack"); return true; } static string longest_prefix(const vector& vals) { if (vals.empty()) return ""; if (vals.size() == 1) return vals[0]; string ref = vals[0]; string retval = ""; for (size_t i = 0; i < ref.length(); ++i) { for (size_t j = 1; j < vals.size(); ++j) { if (i == vals.at(j).length()) return retval; assert(i < vals.at(j).length()); if (vals.at(j).at(i) != ref.at(i)) return retval; } retval += ref[i]; } return retval; } static void annotate_quote(const string_view& data, string* annotated) { int in_quote = 0; char chr[2] = {'_', '\''}; size_t i = 0; annotated->resize(data.length()); while (i < data.length()) { if (data[i] == '\\') { (*annotated)[i] += chr[!!in_quote]; (*annotated)[i + 1] += chr[!!in_quote]; i += 2; continue; } if (data[i] == '\"' || data[i] == '\'') { if (in_quote == data[i]) { (*annotated)[i] += '\"'; ++i; in_quote = 0; continue; } else if (!in_quote) { (*annotated)[i] += '\"'; in_quote = data[i]; ++i; continue; } } (*annotated)[i] += chr[!!in_quote]; ++i; } } static string collapse_quote(const string& data) { static const set chr = { '_', '\'' }; string annotate; string retval; retval.reserve(data.length()); annotate_quote(data, &annotate); for (size_t i = 0; i < data.length(); ++i) { if (chr.count(annotate[i])) retval += data[i]; } return retval; } /* removes backslash escaping from a string. argument one is the string * to be escaped, and argument two is a string listing the symbols to * allow backslash escape. If this is empty, then only the standard * control characters are escaped. If there is a backslash preceeding a * non-escaped symbol or as the last symbol, it is considered invalid * and the original string is returned. */ static string backslash_unescape(const string& data, const string& escapable) { // default escaped symbols static map remap = { {'0', '\0'}, {'a', '\a'}, {'b', '\b'}, {'n', '\n'}, {'r', '\r'}, {'t', '\t'}, {'v', '\v'}, {'\\', '\\'}, }; string ret; ret.reserve(data.length()); for (size_t i = 0; i < data.length(); ++i) { if (data.at(i) == '\\') { ++i; // ends in backslash, not escaped if (i == data.length()) return data; char c = data.at(i); if (remap.count(c)) ret += remap.at(c); else if (escapable.find(c) == string::npos) return data; else ret += c; } else { ret += data.at(i); } } return ret; } static string backslash_escape(const string& data) { // default escaped symbols static map remap = { {'\0', '0'}, {'\a', 'a'}, {'\b', 'b'}, {'\n', 'n'}, {'\r', 'r'}, {'\t', 't'}, {'\v', 'v'}, {'\\', '\\'}, {'\"', '\"'}, {'\'', '\''}, }; string ret; ret.reserve(2 * data.length()); for (size_t i = 0; i < data.length(); ++i) { if (remap.count(data.at(i))) { ret += '\\'; ret += remap.at(data.at(i)); } else { ret += data.at(i); } } return ret; } static string escape_pull(const string& input, size_t* pos, char end_c) { static map remap = { {'0', '\0'}, {'a', '\a'}, {'b', '\b'}, {'n', '\n'}, {'r', '\r'}, {'t', '\t'}, {'v', '\v'}, {'\\', '\\'}, }; string ret; if (input.at(*pos) == end_c) ++*pos; while (*pos < input.length()) { char c = input.at(*pos); if (c == end_c) break; if (c == '\\') { ++*pos; assert(*pos < input.length()); c = input.at(*pos); if (remap.count(c)) { ret += remap.at(c); } else { ret += c; } } else { ret += c; } ++*pos; } return ret; } template static string join(const T& data, const string& delimiter) { vector views; for (const string& x : data) { views.emplace_back(x); } return join(views, delimiter); } static string join(const set& data, const string& delimiter) { if (data.empty()) return ""; stringstream ss; for (const auto &x : data) { ss << x << delimiter; } return ss.str().substr(0, ss.str().length() - delimiter.length()); } static string join(const vector& data, const string& delimiter) { if (data.empty()) return ""; stringstream ss; for (const auto &x : data) { ss << x << delimiter; } return ss.str().substr(0, ss.str().length() - delimiter.length()); } static string join(const vector& data, const string& delimiter) { if (data.empty()) return ""; stringstream ss; for (const auto &x : data) { ss << x << delimiter; } return ss.str().substr(0, ss.str().length() - delimiter.length()); } static string join(const vector& data, const string& delimiter, const set pos) { if (pos.size() == 0) return ""; stringstream ss; for (const auto &x : pos) { ss << data[x] << delimiter; } return ss.str().substr(0, ss.str().length() - delimiter.length()); } static void join(const vector& data, const string& delimiter, const set pos, string* in, string* out) { if (pos.size() == 0) *in = ""; if (pos.size() == data.size()) *out = ""; stringstream ss_in; stringstream ss_out; for (size_t i = 0; i < data.size(); ++i) { if (pos.count(i + 1)) { ss_in << data[i] << delimiter; } else { ss_out << data[i] << delimiter; } } if (pos.size() > 0) *in = ss_in.str().substr( 0, ss_in.str().length() - delimiter.length()); if (pos.size() < data.size()) *out = ss_out.str().substr( 0, ss_out.str().length() - delimiter.length()); } static string replace_first(const string_view& data, const string& find, const string& replacement) { vector pieces; split_with_empty(data, find, &pieces); if (pieces.size() < 2) return string(data); stringstream ss; ss << pieces[0]; ss << replacement; ss << pieces[1]; for (size_t i = 2; i < pieces.size(); ++i) { ss << find << pieces[i]; } return ss.str(); } static string replace(const string_view& data, const string& find, const string& replacement) { vector pieces; split_with_empty(data, find, &pieces); if (pieces.size() < 2) return string(data); stringstream ss; ss << pieces[0]; for (size_t i = 1; i < pieces.size(); ++i) { ss << replacement << pieces[i]; } return ss.str(); } static string remove(const string_view& data, const set& chars) { string ret; ret.reserve(data.length()); for (size_t i = 0; i < data.length(); ++i) { if (!chars.count(data[i])) ret += data[i]; } return ret; } template static int last_token(const string& in, const string& delim, T out) { size_t pos = in.find_last_of(delim); if (pos != string::npos) { extract_one(in.substr(pos + delim.length()), out); return 1; } return 0; } static void add_token(const string_view& data, vector* tokens) { if (data.length()) tokens->push_back(data); } static string trimout(const string_view& data, const string& delimitor) { string ret; ret.reserve(data.length()); vector pieces; split(data, delimitor, &pieces); for (const auto &x : pieces) { ret += x; } return ret; } static string_view trim(const string_view& str, const string& chars) { set skip; for (auto &x : chars) { skip.insert(x); } size_t s = 0; size_t e = str.length() - 1; while (s < str.length() && skip.count(str.at(s))) ++s; if (s == str.length()) return str.substr(0, 0); while (skip.count(str[e])) { if (e == 0) return str.substr(0, 0); --e; } ++e; return str.substr(s, e - s); } static string_view trim(const string_view& str) { size_t s = 0; size_t e = str.length() - 1; while (s < str.length() && isspace(str[s])) ++s; if (s == str.length()) return str.substr(0, 0); while (isspace(str[e])) { if (e == 0) return str.substr(0, 0); --e; } ++e; return str.substr(s, e - s); } /* static bool whitespace(const char& c) { return (c == ' ' || c == '\t' || c == '\r' || c == '\n'); }*/ static bool pop_split(const string& data, char delimiter, int col, string* in, string* out) { int i = 1; size_t start_pos = 0; assert(out); assert(in); *in = ""; for (size_t pos = 0; pos < data.length(); ++pos) { if (i == col) { start_pos = pos; if (start_pos) *in = data.substr(0, start_pos - 1); while (pos < data.length()) { if (data[pos] == delimiter) { *out = data.substr( start_pos, pos - start_pos); if (!in->empty()) *in += delimiter; *in += data.substr(pos + 1); return true; } ++pos; } *out = data.substr(start_pos); return true; } if (data[pos] == delimiter) ++i; } *out = ""; return false; } static bool fast_split(const string& data, char delimiter, int col, string* out) { int i = 1; size_t start_pos = 0; assert(out); for (size_t pos = 0; pos < data.length(); ++pos) { if (i == col) { start_pos = pos; while (pos < data.length()) { if (data[pos] == delimiter) { *out = data.substr( start_pos, pos - start_pos); return true; } ++pos; } *out = data.substr(start_pos); return true; } if (data[pos] == delimiter) ++i; } *out = ""; return false; } static void numset(const string& nums, set* out, int shift) { set tmp; numset(nums, &tmp); for (const auto &x : tmp) { out->insert(x + shift); } } static void numset(const string& nums, set* out) { if (nums == "-") return; assert(out); vector cols; split(nums, ",", &cols); for (auto &x : cols) { size_t val; extract_one(x, &val); out->insert(val); } } static void split(const string_view& data, const string& deliminator, set* tokens) { vector result; split(data, deliminator, &result); for (const auto& x : result) { tokens->insert(x); } } static void split_mind_quote_and_copy(const string_view& data, const string& deliminator, vector* tokens) { vector result; split_mind_quote(data, deliminator, &result); for (const auto& x : result) { tokens->emplace_back(string(x)); } } static void split_and_copy(const string& data, const string& deliminator, vector* tokens) { return split_and_copy(string_view(data), deliminator, tokens); } static void split_and_copy(const string_view& data, const string& deliminator, vector* tokens) { vector result; split(data, deliminator, &result); for (const auto& x : result) { tokens->emplace_back(string(x)); } } static void split(const string_view& data, const string& deliminator, vector* tokens) { assert(deliminator.length()); size_t pos = 0; while (true) { assert(pos <= data.length()); size_t start = pos; pos = data.find(deliminator, start); if (pos == string::npos) { add_token(data.substr(start), tokens); break; } add_token(data.substr(start, pos - start), tokens); pos += deliminator.length(); } } static void split_with_empty(const string_view& data, const string& deliminator, vector* tokens) { assert(deliminator.length()); size_t pos = 0; while (true) { size_t start = pos; pos = data.find(deliminator, start); if (pos == string::npos) { tokens->push_back(data.substr(start)); break; } tokens->push_back(data.substr(start, pos - start)); pos += deliminator.length(); } } static void split_mind_quote(const string_view& data, const string& deliminator, vector* tokens) { size_t pos = 0; string quote; annotate_quote(data, "e); size_t start = pos; while (true) { pos = data.find(deliminator, pos); if (pos == string::npos) { add_token(data.substr(start), tokens); break; } if (quote.at(pos) == '_') { add_token(data.substr(start, pos - start), tokens); start = pos + deliminator.length(); } pos += deliminator.length(); } } static size_t extract_outer_paired(const string& left, const string& right, const string_view& data, vector* results) { assert(results); assert(left != right); size_t curpos = 0; while (curpos < data.size()) { curpos = data.find(left, curpos); if (curpos == string::npos) return results->size(); int depth = 1; size_t start = curpos++; while (curpos < data.length() && depth) { if (substreq(data, curpos, right)) { --depth; curpos += right.length(); } else if (substreq(data, curpos, left)) { ++depth; curpos += left.length(); } else ++curpos; } if (depth) return results->size(); results->push_back(data.substr(start, curpos - start)); } return results->size(); } static bool substreq(const string& main, size_t pos, const string& search) { return !strncmp(main.c_str() + pos, search.c_str(), search.length()); } /* for string views can use substr efficiently */ static bool substreq(const string_view& main, size_t pos, const string_view& search) { return main.substr(pos, search.length()) == search; } static size_t extract_all_paired(const string& left, const string& right, const string_view& data, vector* results) { assert(results); assert(left != right); int depth = 0; for (size_t i = 0; i < data.length(); ++i) { if (substreq(data, i, left)) { i += left.length(); assert(!depth); depth = 1; for (size_t j = i; j < data.length(); ++j) { if (substreq(data, j, left)) { ++depth; } else if (substreq(data, j, right)) { --depth; } if (!depth) { assert(i <= j); results->push_back(data.substr( i, j - i)); break; } } if (depth) { depth = 0; } } } return results->size(); } /* re_extract_all takes a regexp re, a string data, and a position pos. It does a match all of the data using re and puts the pos match into queries each go. 0th is full match, 1st is first parenthetic subexp, etc. */ static size_t re_extract_all(const regex& re, const string& data, size_t pos, vector* queries) { assert(queries); sregex_iterator it = sregex_iterator( data.begin(), data.end(), re); while (it != sregex_iterator()) { assert(pos < it->size()); queries->push_back(it->str(pos)); ++it; } return queries->size(); } static size_t extract_all(const string_view& format, const string_view& data, vector* results) { assert(results); int ret; string_view rest = data; assert(format.length()); assert(format.at(format.length() - 1) != '%'); while (true) { string_view tmp, result; ret = extract("%" + string(format) + "%", rest, nullptr, &result, &tmp); if (ret >= 2) results->push_back(result); else break; rest = tmp; } return results->size(); } static list tokenize_for_extract(const string& format) { list tokens; vector tokens_with_empty; split_with_empty(format, "%", &tokens_with_empty); for (size_t i = 0; i < tokens_with_empty.size(); ++i) { if (tokens_with_empty[i].empty()) { if (tokens.empty()) { if (i) throw logic_error("parsing" + format); tokens.push_back(""); if (format.length() > 1 && format[1] != '%') continue; // initial } if (i + 1 == tokens_with_empty.size()) { tokens.push_back(""); continue; } string token = tokens.back(); token += "%" + string(tokens_with_empty[++i]); tokens.pop_back(); tokens.push_back(token); } else { tokens.push_back(string(tokens_with_empty[i])); } } return tokens; } template static size_t extract_with_pos( const string& format, const string_view& data, ARGS... args) { if (data.empty()) return 0; if (format.empty()) return 0; try { list tokens = tokenize_for_extract(format); return Tokenizer::extract_with_pos_impl( 0, format, 0, data, &tokens, 0, args...); } catch (int e) { return 0; } catch (const logic_error& e) { return 0; } } template static size_t extract_with_pos_impl(size_t pos_format, const string& format, size_t pos_data, const string_view& data, list* tokens, int cnt, T car, size_t* pos, ARGS... cdr) { if (pos_data == data.length()) return cnt; string token = tokens->front(); tokens->pop_front(); if (!tokens->size()) { throw logic_error("parse error"); } if (!(check_token_match( data.substr(pos_data, token.length()), format.substr(pos_format, token.length())))) { // this only happens if the first fixed part of the // string is a non-matching component assert(!cnt); return cnt; } pos_format += token.length(); pos_data += token.length(); size_t nextpos = token_match(data, tokens->front(), pos_data); if (nextpos == string::npos || tokens->front().empty()) nextpos = data.length(); Tokenizer::extract_one( data.substr(pos_data, nextpos - pos_data), car); *pos = pos_data; pos_data = nextpos; pos_format += 1; return Tokenizer::extract_with_pos_impl( pos_format, format, pos_data, data, tokens, cnt + 1, cdr...); } // base case static size_t extract_with_pos_impl( size_t pos_format, const string& format, size_t pos_data, const string_view& data, list* tokens, int cnt) { if (tokens->size() != 1) { throw logic_error("in base case, not one token remaining"); } if (pos_data < data.length() && data.substr(pos_data) != tokens->front()) { throw logic_error("extract failed"); } if (format.substr(pos_format) != tokens->front()) { throw logic_error("implementation failure"); } return cnt; } template static size_t extract(const string& format, const string_view& data, ARGS... args) { size_t pos_format = 0; size_t pos_data = 0; if (data.empty()) return 0; if (format.empty()) return 0; try { list tokens = tokenize_for_extract(format); return Tokenizer::extract_impl( pos_format, format, pos_data, data, &tokens, 0, args...); } catch (int e) { return 0; } catch (const logic_error& e) { return 0; } } static size_t token_match(const string_view& data, const string_view& token, int pos) { static set whitespace = {' ', '\f', '\v', '\n', '\r', '\t'}; size_t whitepos = token.find_last_of("\4"); if (whitepos == string::npos) return data.find(token, pos); else if (whitepos == 0) { size_t retval = string::npos; for (char c : whitespace) { string testtoken = string(token); testtoken[0] = c; size_t candidate = data.find(testtoken, pos); if (candidate < retval) retval = candidate; } return retval; } assert(0); return 0; } static bool check_token_match(const string_view& data, const string_view& token) { static set whitespace = {' ', '\f', '\v', '\n', '\r', '\t', '\4'}; if (!token.length()) { if (!data.length()) return true; return false; } if (data.substr(1, data.length()) != token.substr(1, token.length())) return false; if (data[0] == token[0]) return true; if (whitespace.count(data[0]) && whitespace.count(token[0])) return true; return false; } template static size_t extract_impl(size_t pos_format, const string& format, size_t pos_data, const string_view& data, list* tokens, int cnt, T car, ARGS... cdr) { if (pos_data == data.length()) return cnt; string_view token = tokens->front(); tokens->pop_front(); if (!tokens->size()) { throw logic_error("parse error"); } if (!(check_token_match( data.substr(pos_data, token.length()), format.substr(pos_format, token.length())))) { // this only happens if the first fixed part of the // string is a non-matching component assert(!cnt); return cnt; } pos_format += token.length(); pos_data += token.length(); size_t nextpos = token_match(data, tokens->front(), pos_data); if (nextpos == string::npos || tokens->front().empty()) nextpos = data.length(); Tokenizer::extract_one(data.substr(pos_data, nextpos - pos_data), car); pos_data = nextpos; pos_format += 1; return Tokenizer::extract_impl( pos_format, format, pos_data, data, tokens, cnt + 1, cdr...); } /* string specialization ensures that whitespace is preserved. */ static void extract_one(const string_view& in, string* out) { if (!out) return; *out = in; } static void extract_one(const string_view& in, string_view* out) { if (!out) return; *out = in; } static void extract_one(const string_view& in, bool* out) { *out = false; if (in == "true") *out = true; if (in == "1") *out = true; if (in == "on") *out = true; if (in == "yes") *out = true; return; } static void extract_one([[maybe_unused]] const string_view& in, [[maybe_unused]] nullptr_t out) { return; } template static void extract_one(const string_view& in, T out) { if (!out) return; stringstream ss; ss << in; ss >> *out; } // base case static size_t extract_impl(size_t pos_format, const string& format, size_t pos_data, const string_view& data, list* tokens, int cnt) { if (tokens->size() != 1) { throw logic_error("in base case, not one token remaining"); } if (pos_data < data.length() && data.substr(pos_data) != tokens->front()) { throw logic_error("extract failed"); } if (format.substr(pos_format) != tokens->front()) { throw logic_error("implementation failure"); } return cnt; } template static int get_split_matching(vector* out, const string& delimiter, const string_view& data, Args... args) { vector tokens; split(data, delimiter, &tokens); return get_lines_matching_impl(out, tokens, args...); } template static int get_lines_matching(vector* out, const string_view& data, Args... args) { vector tokens; split(data, "\n", &tokens); return get_lines_matching_impl(out, tokens, args...); } template static int get_lines_matching_impl(vector* out, const vector& lines, const string& car, Args... cdr) { vector result; get_lines_matching_impl(&result, lines, cdr...); for (auto &x : result) { if (x.find(car) != string::npos) { out->push_back(x); } } return out->size(); } static int get_lines_matching_impl(vector* out, const vector& lines) { assert(out); *out = lines; return out->size(); } template static string space(const string& value, const string& spacer, Args... args) { stringstream ss; space_impl(value, spacer, &ss, args...); return ss.str(); } template static void space_impl(const string& value, const string& spacer, stringstream* ss, size_t car, Args... cdr) { assert(ss); assert(car <= value.length()); *ss << value.substr(0, car); if (car < value.length()) { *ss << spacer; space_impl(value.substr(car), spacer, ss, cdr...); } } static void space_impl(const string& value, [[maybe_unused]] const string& spacer, stringstream* ss) { *ss << value; } static string make_list(const string& hex, size_t digits) { if (!(digits == 2 || digits == 4 || digits == 8)) return ""; if (hex.length() % digits) return ""; stringstream ssout; for (size_t i = 0; i < hex.length(); i += digits) { stringstream ss; int64_t val = stoul(hex.substr(i, digits), nullptr, 16); if (val > (1 << (digits * 4 - 1))) val -= (1 << (digits * 4)); ssout << val; if (i < hex.length() - digits) ssout << ", "; } return ssout.str(); } /* Returns true if value matches a wildcard format using * and ?, where * matches 0 or more characters and ? matches exactly one */ static bool wildcard_match(const string& format, const string& value) { size_t N = format.length() + 1 + value.length() + 1; multimap> paths; paths.insert(make_pair(0, make_pair(0, 0))); auto it = paths.begin(); while (it != paths.end() && it->first < N) { const auto& coord = *it; if (format[coord.second.first] == '*') { if (coord.second.first + 1 <= format.length()) { paths.insert(paths.end(), make_pair( coord.first + 1, make_pair(coord.second.first + 1, coord.second.second))); } if (coord.second.second + 1 <= value.length()) { paths.insert(paths.end(), make_pair( coord.first + 1, make_pair(coord.second.first, coord.second.second + 1))); } } else if (format[coord.second.first] == '?' || format[coord.second.first] == value[coord.second.second]) { paths.insert(paths.end(), make_pair(coord.first + 2, make_pair(coord.second.first +1, coord.second.second+1))); } ++it; } return paths.count(N); } }; #endif // __TOKENIZER__H__ logserver/src/typing_line.h000066400000000000000000000152471512456611200163630ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __TYPING_LINE__H__ #define __TYPING_LINE__H__ #include #include #include #include "constants.h" using namespace std; /* TypingLine encapsulates the notion of the user typing, e.g., a search term, * and handles the keystrokes and what the string is being created */ class TypingLine { public: TypingLine() : TypingLine(true) {} explicit TypingLine(bool completable) : _completable(completable) { reset(); } virtual ~TypingLine() {} /* process a keystroke from curses's getch(). returns true if it has a * result, which happens when either enter or escape was hit, meaning * that the interface should terminate the use of the typing line and * accept or reject input */ virtual bool process(int c) { if (c == 27) { // escape set_result(false); return true; } else if (c == 330) { // delete del_char(); } else if (c == KEY_ENTER || c == '\n') { set_result(true); return true; } else if (c == KEY_BACKSPACE || c == 127 || c == '\b') { // backspace, remove a character pop_char(); } else if (c > 0 && c < 256 && isprint(c)) { // add the character to what is typed push_char(c); } else if (c == KEY_LEFT) { // move cursor left unless at start of line commit_complete(); if (_pos == G::NO_POS) _pos = _type.length(); --_pos; // returns to NO_POS if empty } else if (c == KEY_RIGHT) { // move cursor right unless at end of line commit_complete(); if (_pos != G::NO_POS) { ++_pos; if (_pos == _type.length()) _pos = G::NO_POS; } } else if (c == KEY_HOME) { // go to start of line commit_complete(); if (_type.size()) _pos = 0; } else if (c == KEY_END) { // go to end (append pos) commit_complete(); _pos = G::NO_POS; } else if (c == '\t' && _completable) { // try to tab complete the word if that is enabled tab_complete(); } return _result == false; } // true if _result is not nullopt virtual bool has_result() const { return _result != nullopt; } // true if it ended with enter, false if escape virtual bool result() const { assert(_result); return *_result; } /* returns what was typed so far */ virtual string typed() const { if (_completed.empty()) return _type; return _completed; } /* sets what should be considered typed */ virtual void set_type(const string_view& val) { _type = string(val); _completed = ""; } /* clears what was typed and resets the cursor to the end */ virtual void reset() { _type = ""; _completed = ""; _pos = G::NO_POS; _result = nullopt; _only_numbers = false; } /* for search on a line number to drop other letters */ virtual void only_numbers() { _only_numbers = true; _completable = false; } /* returns the cursor position in the line, for e.g., insertion. * position of G::NO_POS means its at the end of what is typed */ virtual size_t get_pos() const { if (_pos != G::NO_POS) return _pos; if (!_completed.empty()) return _completed.length(); return _type.length(); } protected: virtual void commit_complete() { if (_completed.empty()) return; _type = _completed; _completed = ""; } /* the user has finished typing. result is true for enter meaning accept * the typed value, false for escape meaning reject it */ virtual void set_result(bool result) { _result = result; commit_complete(); if (*_result) { auto* results = completes(); results->insert(_type); } } virtual void pop_char() { commit_complete(); if (_pos == G::NO_POS && _type.length()) { _type = _type.substr(0, _type.length() - 1); } else if (_pos != G::NO_POS && _pos != 0) { assert(_pos < _type.length()); _type = _type.substr(0, _pos - 1) + _type.substr(_pos); --_pos; } } // removes a character from the typed value virtual void del_char() { commit_complete(); if (_pos == G::NO_POS) return; if (_pos + 1 == _type.length()) { _type = _type.substr(0, _pos); } else { _type = _type.substr(0, _pos) + _type.substr(_pos + 1); } if (_pos == _type.length()) _pos = G::NO_POS; } // appends a character to the typed value virtual void push_char(char c) { commit_complete(); if (_only_numbers && (c < '0' || c > '9')) return; if (_pos == G::NO_POS) { _type += c; } else { assert(_pos < _type.length()); _type = _type.substr(0, _pos) + c + _type.substr(_pos); ++_pos; assert(_pos < _type.length()); } } /* while iterating over possible tab values, return true if this * candidate has the current typed value as prefix */ virtual bool tab_valid(const string& completion) { return completion.find(_type) == 0; } /* use prior search terms in session as tab completes to make the * stack model of adding and removing searchterms easier to remove one * from the top */ virtual void tab_complete() { set* options = completes(); if (!_completed.empty()) { /* with repeated tabs, progress to next choice */ auto it = options->lower_bound(_completed); ++it; if (it != options->end() && tab_valid(*it)) { _completed = *it; return; } // otherwise we've hit the last candidate. restart the // candidate search at the first result, same as if // there was not value } auto it = options->lower_bound(_type); if (it == options->end()) return; _completed = *it; } /* TODO: future search term configurable defaults for personal work load */ static inline set* completes() { // TODO have persistent options and a file for specific use // cases static set _completes = {}; return &_completes; } // what the user has typed or accepted string _type; // a tentative tab completion value that can be iterated over with tabs // or accepted with other keystrokes string _completed; // cursor insertion point size_t _pos; // set if this line has a result, true if result reached with enter, // false if reached with esc. optional _result; // true if typing should only allow numbers bool _only_numbers; // true if tab complete is allowed bool _completable; }; #endif // __TYPING_LINE__H__ logserver/src/utf8.h000066400000000000000000000033211512456611200147160ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __UTF8__H__ #define __UTF8__H__ #include using namespace std; /* static method to perform utf8 simplifications when using logserver to view * man pages */ class UTF8 { public: // if we can decode a utf8 symbol in string sv at position i, then // return it and advance i. otherwise return nullopt. static optional simplify(const string_view& sv, size_t* i) { if (*i + 2 >= sv.size()) [[unlikely]] return nullopt; if (static_cast(sv[*i]) != 0xe2) [[likely]] return nullopt; if (static_cast(sv[*i + 1]) == 0x80) { uint8_t c = static_cast(sv[*i + 2]); static const map vals = { {0x90, "-"}, {0x93, "-"}, {0x94, "-"}, {0x98, "'"}, {0x99, "'"}, {0x9c, "\""}, {0x9d, "\""}, {0xa2, "*"}, {0xa6, "..."}, }; // if we have a match, return the value and advance i auto it = vals.find(c); if (it != vals.end()) { *i += 2; return it->second; } } return nullopt; } }; #endif // __UTF8__H__ logserver/src/version.h000066400000000000000000000016771512456611200155310ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __VERSION__H__ #define __VERSION__H__ #include using namespace std; /* Provides a string codename for major version changes */ class Version { public: static inline const string VERSION = "maple"; }; #endif // __VERSION__H__ logserver/src/view.h000066400000000000000000000040051512456611200150020ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __VIEW__H__ #define __VIEW__H__ #include #include #include #include #include #include #include "config.h" #include "constants.h" #include "epoch.h" #include "filter_manager.h" #include "format_string.h" #include "log_lines.h" #include "match.h" #include "navigation.h" #include "renderer.h" using namespace std; using namespace std::placeholders; /* View holds an element of the stack of logs in logserer. Each view has its own * navigation, filterer, and loglines. When different views are added, old ones * track their keyword being sought and where the navigation is when the user * returns. */ class View { public: // contructs a view with the loglines, filter runner, and navigation // trinity. View(LogLines* ll, FilterManager* fm, Navigation* navi) : _ll(ll), _fm(fm), _navi(navi) { } // returns the filter manager for this view FilterManager* fm() { return _fm.get(); } // returns the loglines for this view LogLines* ll() { return _ll.get(); } // returns the navigation for this view Navigation* navi() { return _navi.get(); } protected: // the loglines instance unique_ptr _ll; // the filter manager instance unique_ptr _fm; // the navigation instance unique_ptr _navi; }; #endif // __VIEW__H__ logserver/src/view_stack.h000066400000000000000000000335321512456611200161760ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __VIEW_STACK__H__ #define __VIEW_STACK__H__ #include #include #include #include #include #include "config.h" #include "constants.h" #include "epoch.h" #include "filter_renderer.h" #include "filter_manager.h" #include "log_lines.h" #include "match.h" #include "navigation.h" #include "renderer.h" #include "view.h" using namespace std; using namespace std::placeholders; /* ViewStacks holds a list of Views and tracks which one is the current one that * should be rendered. It also holds a render thread worker that goes into the * current view and runs its rendering code */ class ViewStack { public: /* empty constructor */ ViewStack() : _tab(false), _tab_key('\t') {} /* initialize the view stack with the renderer and epoch, and starts the * worker thread */ virtual void init(Renderer* renderer, Epoch* epoch) { _renderer = renderer; _epoch = epoch; _cur = nullopt; _filter_renderer.reset(new FilterRenderer(_renderer, *_epoch)); _worker.reset(new thread(bind(&ViewStack::worker, this))); } /* destructor clears cur, notifies the condition and waits for thread to * finish */ virtual ~ViewStack() { unique_lock ul(_m); _cur = nullopt; _epoch->shutdown(); _cur_cond.notify_all(); ul.unlock(); _worker->join(); } /* accessor functions to get the view or its constituent filterer, * loglines, and navigation based on the current element in the stack */ FilterManager* fm() const { shared_lock ul(_m); return (**_cur)->fm(); } // gets current loglines LogLines* ll() const { shared_lock ul(_m); return (**_cur)->ll(); } // gets current navi Navigation* navi() const { shared_lock ul(_m); return (**_cur)->navi(); } // gets current view View* view() const { shared_lock ul(_m); return (**_cur).get(); } /* trims all views past the current one */ virtual void trim_later() { unique_lock ul(_m); auto it = _cur; stop_render(&ul); trim_later_locked(); _cur = it; start_render(); } /* trims all views past the current ones, assumes write lock is held */ virtual void trim_later_locked() { if (!_cur) return; if (*_cur == _views.end()) return; auto it = *_cur; ++it; _views.erase(it, _views.end()); } /* appends a new view to the stack based around the provided loglines */ virtual void push(LogLines* ll) { _tab_data.clear(); _tab = false; unique_lock ul(_m); stop_render(&ul); trim_later_locked(); unique_ptr navi(new Navigation()); unique_ptr filter(new FilterManager( ll, navi.get())); _views.push_back(make_unique( ll, filter.release(), navi.release())); _cur = _views.end(); // _views will have one element at least from push --*_cur; start_render(); } /* removes the top element of the loglines stack */ virtual void pop() { _tab_data.clear(); _tab = false; unique_lock ul(_m); if (_views.size() <= 1) return; stop_render(&ul); _views.pop_back(); _cur = _views.end(); // _views will have one element at least from check --*_cur; start_render(); } /* returns the title bar for the stack of views and emboldens the * current one */ bool title_bar(FormatString* fs, size_t cols) const { shared_lock ul(_m); // wait until cur is set if rendering is suspended while (!_cur) _cur_cond.wait(ul); fs->add(Version::VERSION, 0); fs->add(" ", 0); for (const auto& x : _views) { int weight = 0; string suffix; if (**_cur == x) { weight = G::BOLD; suffix = tab_summary(); } fs->add("(", weight); fs->add(x->ll()->display_name(), weight); fs->add(suffix); fs->add(") ", weight); } fs->truncate(cols); return true; } /* checks that everything is okay statewise */ bool check() { if (!fm() || !ll() || !navi() || !view()) return false; shared_lock ul(_m); return _cur && _renderer && _epoch; } /* clears current loglines */ virtual void clear() { if (!navi()->at_end()) navi()->start(); ll()->init(); _tab_data.clear(); } /* moves current view left (down) on the stack */ virtual void lshift() { _tab_data.clear(); _tab = false; if (ll()->is_static("help")) return pop(); unique_lock ul(_m); if (*_cur == _views.begin()) return; auto it = *_cur; stop_render(&ul); --it; _cur = it; start_render(); } /* moves current view right (up) on the stack */ virtual void rshift() { _tab_data.clear(); _tab = false; auto it = *_cur; unique_lock ul(_m); stop_render(&ul); ++it; if (it == _views.end()) --it; _cur = it; start_render(); } /* hits enter on logline's current line */ virtual void enter() { ll()->enter(navi()->cur()); } /* breaks the current line */ virtual void break_line() { if (ll()->length() == 0) return; ll()->split(navi()->cur()); } /* toggles pinning on the current line */ virtual void pin_line() { if (ll()->length() == 0) return; if (navi()->at_end()) return; fm()->toggle_pin(navi()->cur()); } /* creates a new loglines based on the current view. If we have search * terms applied, use that filtered view as the current view. If we do * not have them applied, e.g., we are in ALL mode, go to the nearest * pinned lines in both directions and use that as the border for the * new loglines */ virtual void permafilter() { if (ll()->length() <= 1) return; if (_tab && tab_suppressed()) { trim_later(); push(ll()->tabfilter_clone( tab_summary().substr(6), _tab_key, _tab_suppress)); unique_lock ul(_m); _tab_suppress.clear(); _tab_data.clear(); } else if (fm()->is_mode_all()) { // only permafilter between pins trim_later(); auto pins = fm()->nearest_pins(navi()->cur()); push(ll()->pinfilter_clone("-", pins.first, pins.second)); } else { set lines; fm()->get_view(&lines); if (lines.empty()) return; trim_later(); push(ll()->permafilter_clone(lines, fm()->filter_string())); } } // show the help loglines virtual void help() { if (ll()->is_static("help")) return; push(LogLines::show_static("help")); navi()->start(); navi()->goto_line(10, false); } // inserts a dashed line either appending if we are at the end or // inserting at our position virtual void insert_dash_line() { if (ll()->length() == 0) return; if (navi()->at_end()) { fm()->set_pin(ll()->add_line(G::DASH)); } else { size_t pos = navi()->cur(); ll()->insert_line(G::DASH, pos); fm()->set_pin(pos); } } // if we've changed the current line, revert it to the original value virtual void backspace() { unique_lock ul(_m); auto it = _cur; // we need to stop rendering to get workers outside of the line // filter keyword that we may be removing stop_render(&ul); if ((**it)->fm()->pop_keyword()) { // don't do the else } else if (!(**it)->navi()->at_end()) { (**it)->ll()->revert((**it)->navi()->cur()); } _cur = it; start_render(); } // if we've changed the current line, revert it to the original value virtual void finish_match(bool accepted) { unique_lock ul(_m); auto it = _cur; // we need to stop rendering to get workers outside of the line // filter keyword that we may be removing if not accepted or the // keyword is empty stop_render(&ul); (**it)->fm()->finish_match(accepted); _cur = it; start_render(); } // follow xrefs for the line we are one virtual void follow(bool display_all) { if (ll()->length() == 0) return; pair where = ll()->follow( navi()->cur(), display_all); if (!where.first) { navi()->jump_back(); return; } if (where.first != ll()) { trim_later(); push(where.first); navi()->goto_line(where.second, false); } else { navi()->goto_line(where.second, true); } } /* Runs a pipe command and puts the stdout as a new view */ virtual void run_pipe_command(const string& command) { try { Run *runner = new Run(command); (*runner)(); trim_later(); // write cur loglines into next set lines; fm()->get_view(&lines); ll()->stream_write(runner, lines); push(new LogLines(runner)); } catch (const logic_error& lg) {} } /* Merge the next line to the end of hte current one */ virtual void merge() { ll()->merge(navi()->cur()); } virtual void tab_toggle() { _tab = !_tab; if (!_tab) return; _tab_data.clear(); unique_lock ul(_m); _tab_suppress.clear(); } virtual void tab_key(char c) { _tab_key = c; _tab = true; _tab_data.clear(); } virtual void tab_suppress(size_t pos) { if (!_tab) return; unique_lock ul(_m); if (_tab_suppress.count(pos)) { _tab_suppress.erase(pos); } else { _tab_suppress.insert(pos); } } protected: // stops rendering because we are going to change the current view. // tells the worker to stop and waits virtual void stop_render(unique_lock* ul) { _cur = nullopt; _epoch->advance(); ul->unlock(); // spin lock until the worker gets the message while (true) { while (_worker_busy) this_thread::yield(); ul->lock(); if (!_worker_busy) break; } } // signals worker or interface waiting on _cur being nullopt that it is no longer // the case virtual void start_render() { _cur_cond.notify_all(); } // worker thread. checks the current epoch, when it advances triggers a // render, unless there is no _cur view. This happens when the views are // changing. Wait until there is a cur, check to make sure we aren't // shutting down, and then perform a render on the current view. If // epoch changes during the render, abort the current render and // restart void worker() { _worker_busy = false; // pretend we've render epoch 0 < first epoch (1) size_t cur_epoch = 0; while (true) { unique_lock ul(_m); while (cur_epoch == _epoch->cur()) { _epoch->wait(&ul); } if (_epoch->cur() == G::NO_POS) return; while (!_cur) { if (_epoch->cur() == G::NO_POS) return; _cur_cond.wait(ul); } assert(cur_epoch < _epoch->cur() && _cur); cur_epoch = _epoch->cur(); // check if we are quitting and abort if (cur_epoch == G::NO_POS) return; RenderParms* rp = new RenderParms(); _renderer->set_render_parms(rp); (**_cur)->fm()->set_render_parms(rp); rp->total_length = (**_cur)->ll()->length(); rp->ll = (**_cur)->ll(); rp->navi = (**_cur)->navi(); rp->cur_epoch = cur_epoch; rp->tab_key = nullopt; rp->tab_data = nullptr; if (_tab) { rp->tab_key = _tab_key; rp->suppressed_tabs = _tab_suppress; rp->tab_data = &_tab_data; } _worker_busy = true; ul.unlock(); // busy signals there is ongoing rendering of whatever // _cur pointed to when we first started. _filter_renderer->run_render(rp); _worker_busy = false; } } virtual string tab_summary() const { if (!_tab || _tab_suppress.empty()) return ""; stringstream ret; auto it = _tab_suppress.end(); --it; ret << " cols="; size_t i = 0; optional start; for (; i < *it + 2; ++i) { if (_tab_suppress.count(i)) { if (start == nullopt) continue; if (start == i - 1) ret << i << ","; else ret << (*start + 1) << "-" << i << ","; start = nullopt; continue; } if (start == nullopt) start = i; } if (start) ret << *start + 1; else ret << i + 2; ret << "+"; return ret.str(); } virtual bool tab_suppressed() const { unique_lock ul(_m); return _tab_suppress.size(); } // thread safety for render thread and changing views mutable shared_mutex _m; // pointer to the renderer object Renderer* _renderer; // pointer to the epoch object Epoch* _epoch; // list of the Views in this stack. Each view manages it own filter // runner, loglines, navigation, and the matching keywords list> _views; // if nullopt, then no view is current. otherwise points to the current // view that should be rendered by the render worker optional>::iterator> _cur; // when _cur is set from nullopt back to a valid value, this is // signalled. worker waits on a nullopt cur until it is set mutable condition_variable_any _cur_cond; // true when the worker is actively rendering. used to wait until an // aborted render is terminated and the worker goes back to waiting so // that cur can be changed with trying to render for another View atomic _worker_busy; // the thread that calls run_render when the epoch advances and there is // a valid current view unique_ptr _worker; // the renderer that draws onto the screen the current view unique_ptr _filter_renderer; // whether custom tab char is enabled atomic _tab; // the tab char if enabled. not optional since tab key needs to be // remembered even if it is off atomic _tab_key; // set of suppressed tab columns for rendering set _tab_suppress; // tab data preserved during rendering to account col widths TabData _tab_data; }; #endif // __VIEW__H_ logserver/src/xref.h000066400000000000000000000055401512456611200150010ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __XREF__H__ #define __XREF__H__ #include #include #include #include #include #include #include #include #include #include using namespace std; /* reads lines to see if they are actionable links */ class XRef { public: static bool grep_line(const string_view& line, string* file, size_t* lineno) { int matched = Tokenizer::extract("%:%:%", line, file, lineno, nullptr); if (*lineno == 0) return false; // grep counts from one --*lineno; if (matched < 2) return false; if (filesystem::exists(*file)) return true; return false; } static bool storytime_line(const string_view& line, string* file, size_t* lineno) { if (!line.length()) return false; if (line[0] != '@') return false; size_t last = line.find_last_of(':'); if (last == string::npos || last == 1) { return false; } try { *lineno = stoi(string(line.substr(last + 1))); } catch (...) { return false; } *file = line.substr(1, last - 1); if (filesystem::exists(*file)) return true; return false; } static bool link_line(const string_view& line, string* file, size_t* lineno) { int matched = Tokenizer::extract("%@%:% %", line, nullptr, file, lineno, nullptr); return matched >= 3; } static bool ctags_line(const string& filename, const string_view& line, string* file, size_t* lineno) { string item, target, ex; int ret = Tokenizer::extract("%\t%\t%", line, &item, &target, &ex); if (ret < 3) return false; if (item.starts_with("!_TAG_")) return false; filesystem::path p{filename}; filesystem::path dir = p.parent_path(); if (!filesystem::exists(dir / target)) return false; *file = dir / target; if (!ex.starts_with("/^") || ex.find("$/;\"") == string::npos) { // TODO handle these if ever relevant return false; } ex = ex.substr(2, ex.find("$/;\"") - 2); ifstream fin(*file); string curline; size_t i = 0; while (getline(fin, curline)) { if (curline == ex) { *lineno = i; break; } ++i; } return true; } }; #endif // __XREF_CONTEXT__H__ logserver/src/zcat.h000066400000000000000000000077241512456611200150040ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __IB__ZCAT__H__ #define __IB__ZCAT__H__ #include #include #include #include #include #include "zlib.h" using namespace std; /* helper class for using zlib. */ class ZCat { public: /* gunzip the data string */ static string zcat(const string& data) { return zcat(data, nullptr); } /* gunzip the data string and count how much wasn't gunziped */ static string zcat(const string& data, size_t* leftover) { uint8_t buf[1 << 17]; return zcat(data, leftover, buf, 1 << 17, 0); } // returns true if the gzip magic number matches at start static bool magic(const string& str) { return magic(str, 0); } // returns true if the zlib or gzip magic number matches at position pos static bool magic(const string& str, size_t pos) { return magic_zlib(str, pos) || magic_gzip(str, pos); } // returns true if the gzip magic number matches at position pos static bool magic_gzip(const string& str, size_t pos) { if (pos + 2 >= str.length()) return false; return str[pos] == (char) 0x1f && str[pos + 1] == (char) 0x8b && str[pos + 2] == (char) 0x08; } // returns true if the zlib magic number matches at position pos static bool magic_zlib(const string& str, size_t pos) { if (pos + 1 >= str.length()) return false; if (str[pos] == (char) 0x78 && str[pos + 1] == (char) 0x9c) return true; if (str[pos] == (char) 0x78 && str[pos + 1] == (char) 0x5e) return true; if (str[pos] == (char) 0x78 && str[pos + 1] == (char) 0xda) return true; return false; } // tries to gunzip as much of data as it can, assuming there is some // gzipped data at the start of data but unclear where it ends static string zcat(const string& data, size_t* leftover, uint8_t *out, size_t CHUNK, int mode = 0) { int ret; unsigned have; z_stream strm; strm.zalloc = Z_NULL; strm.zfree = Z_NULL; strm.opaque = Z_NULL; strm.avail_in = 0; strm.next_in = Z_NULL; // zlib / gzip have a terrible design interface // 15 is the window size for memory usage // if you negate it, then its headerless and the // negative value is the window size // if you add 16 then it does gzip decoding // with the value minus 16 being window size // if you add 32 it does either gzip or zlib by // checking headers and the value minus 32 is the // window size. int param = 15 + 16 + 16; if (mode == 1) param = -15; if (mode == 2) param = 33; if (mode == 3) param = -1; ret = inflateInit2(&strm, param); if (ret != Z_OK) return ""; stringstream ss; strm.avail_in = data.length(); strm.next_in = reinterpret_cast(const_cast(data.data())); do { strm.avail_out = CHUNK; strm.next_out = out; ret = inflate(&strm, Z_FINISH); if (ret == Z_STREAM_ERROR) return ss.str(); if (ret == Z_NEED_DICT) return ss.str(); have = CHUNK - strm.avail_out; if (ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) { ss << string(reinterpret_cast(out), have); if (leftover) *leftover = strm.avail_in; inflateEnd(&strm); return ss.str(); } ss << string(reinterpret_cast(out), have); } while (strm.avail_out == 0); if (leftover) *leftover = strm.avail_in; inflateEnd(&strm); return ss.str(); } }; #endif // __IB__ZCAT__H__ logserver/tests/000077500000000000000000000000001512456611200142335ustar00rootroot00000000000000logserver/tests/mock_log_lines.h000066400000000000000000000042301512456611200173670ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __MOCK_LOG_LINES__H__ #define __MOCK_LOG_LINES__H__ #include #include using namespace std; class MockLogLines : public ILogLines { public: MockLogLines() : _eof(false) {} virtual ~MockLogLines() { CHECK(_lines.empty()); } virtual size_t add_line(const string& line) { unique_lock ul(_m); CHECK(_lines.size()); CHECK(_lines.front() == line); _lines.pop_front(); return 0; } // adds a Line object to log lines virtual size_t add_line(Line* line) { unique_lock ul(_m); return add_line(line->get()); } // adds Line objects to log lines virtual void add_lines(list* lines) { for (Line* line : *lines) { add_line(line->get()); delete line; } lines->clear(); } // sets eof if input reader is done virtual bool eof() const { unique_lock ul(_m); return _eof; } // unset eof, e.g., if the file being tailed is updated virtual void set_eof(bool value) { unique_lock ul(_m); _eof = value; if (_eof) CHECK(!_lines.size()); } virtual void expect(const list& lines) { unique_lock ul(_m); _lines = lines; } virtual void expect(const string& line) { unique_lock ul(_m); _lines.push_back(line); } virtual bool no_expectations() { unique_lock ul(_m); return _lines.empty(); } protected: list _lines; bool _eof; mutable mutex _m; }; #endif // __MOCK_LOG_LINES__H__ logserver/tests/mock_log_lines_for_filter.h000066400000000000000000000075771512456611200216230ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef __MOCK_LOG_LINES_FOR_FILTER__H__ #define __MOCK_LOG_LINES_FOR_FILTER__H__ #include #include #include #include #include "log_lines.h" using namespace std; class MockLogLinesForFilter : public LogLines { public: MockLogLinesForFilter(size_t len, int anchor, const string& match_template) : _total_checked(0), _anchor(anchor), _match_template(match_template), _flag_check(true) { _lines.resize(len); _checked.resize(len); } MockLogLinesForFilter(size_t len, int anchor) : _total_checked(0), _anchor(anchor), _flag_check(false) { _lines.resize(len); _checked.resize(len); } virtual ~MockLogLinesForFilter() { } virtual size_t length_locked() const { return _lines.size(); } virtual inline string_view get_line_locked(size_t pos) const { if (pos >= _lines.size()) return ""; if (_flag_check) REQUIRE(!_checked.at(pos)); ++_total_checked; ++_checked[pos]; if (_lines[pos].empty()) create(pos); return _lines[pos]; } virtual size_t range_add_if(size_t start, size_t end, set* results, function fn) const { if (start > end) { size_t swp = start; start = end; end = swp; } shared_lock ul(_m); if (start >= _lines.size()) return _lines.size(); if (end > _lines.size()) end = _lines.size(); // simulate reading the line for (size_t i = start; i < end; ++i) { if (fn(get_line_locked(i))) results->insert(i); } return _lines.size(); } virtual bool checked_all() const { unique_lock ul(_m); return _total_checked == _lines.size(); } virtual bool checked_number(size_t number) const { unique_lock ul(_m); return _total_checked == number; } virtual void expect_match(size_t pos) { unique_lock ul(_m); stringstream ss; if (_anchor & Match::ANCHOR_LEFT) { ss << _match_template << " " << pos; } else if (_anchor & Match::ANCHOR_RIGHT) { ss << pos << " " << _match_template; } else { ss << pos << " " << _match_template << " "; } _lines[pos] = ss.str(); } virtual void expect(const string& match, size_t number) { unique_lock ul(_m); mt19937 rng(random_device{}()); uniform_int_distribution dist( 0, _lines.size() - 1); while (number) { size_t pos = dist(rng); if (_lines[pos].empty()) { --number; _lines[pos] = match; } } } void dump() { unique_lock ul(_m); for (size_t i = 0; i < _lines.size(); ++i) { if (_lines[i].empty()) continue; cout << i << " " << _lines[i] << endl; } } protected: void create(size_t pos) const { stringstream ss; // add half of them being matches for the wrong anchor if (pos % 4 == 0) ss << " "; if (_anchor & Match::ANCHOR_RIGHT && pos % 2 == 0) ss << _match_template; ss << "line " << pos; if (_anchor & Match::ANCHOR_LEFT && pos % 2 == 0) ss << _match_template; if (pos % 4 == 0) ss << " "; _lines[pos] = ss.str(); } mutable size_t _total_checked; int _anchor; mutable vector _checked; mutable vector _lines; string _match_template; bool _flag_check; }; #endif // __MOCK_LOG_LINES_FOR_FILTER__H__ logserver/tests/test.h000066400000000000000000000001711512456611200153620ustar00rootroot00000000000000#define CATCH_CONFIG_MAIN #include #include #include #include logserver/tests/test_base64.cc000066400000000000000000000011401512456611200166610ustar00rootroot00000000000000#include #include #include #include #include "test.h" #include "base64.h" using namespace std; /* randomly convert to and from base64, should be the same */ TEST_CASE("base64 random use") { mt19937 rng(random_device{}()); uniform_int_distribution char_dist; for (int i = 0; i < 100; ++i) { uniform_int_distribution len_dist(10, 1000); size_t len = len_dist(rng); string s; s.reserve(len); for (size_t j = 0; j < len; ++j) { char c = static_cast(char_dist(rng)); s += c; } CHECK(Base64::decode(Base64::encode(s)) == s); } } logserver/tests/test_config.cc000066400000000000000000000013531512456611200170500ustar00rootroot00000000000000#include "test.h" #include "config.h" TEST_CASE("config settings missing") { Config* conf = Config::_(); CHECK(conf->get("key").empty()); } TEST_CASE("config settings present") { Config* conf = Config::_(); conf->set("key1", "value1"); conf->set("key2", "value2"); CHECK(conf->get("key").empty()); CHECK(conf->get("key1") == "value1"); CHECK(conf->get("key2") == "value2"); } TEST_CASE("config boolean") { Config* conf = Config::_(); conf->flag_on("btrue"); conf->flag_off("bfalse"); CHECK(conf->flag("btrue")); CHECK(!conf->flag("bfalse")); CHECK(!conf->flag("bmissing")); conf->flag_toggle("btrue"); conf->flag_toggle("bfalse"); CHECK(!conf->flag("btrue")); CHECK(conf->flag("bfalse")); CHECK(!conf->flag("bmissing")); } logserver/tests/test_epoch.cc000066400000000000000000000011321512456611200166740ustar00rootroot00000000000000#include #include #include #include "test.h" #include "epoch.h" using namespace std; TEST_CASE("advance") { Epoch e; CHECK(e.cur() == 1); e.advance(); CHECK(e.cur() == 2); e.advance(); CHECK(e.cur() == 3); e.advance(); CHECK(e.cur() == 4); } void epoch_waiter(Epoch* e) { mutex m; unique_lock ul(m); size_t cur = e->cur(); e->wait(&ul); CHECK(e->cur() == cur + 1); } TEST_CASE("wait") { Epoch e; e.advance(); e.advance(); e.advance(); e.advance(); e.advance(); thread t(bind(&epoch_waiter, &e)); usleep(1000); e.advance(); t.join(); } logserver/tests/test_event_bus.cc000066400000000000000000000113521512456611200175750ustar00rootroot00000000000000#include "test.h" #include #include #include "event_bus.h" #include "i_log_lines_events.h" #include "line.h" using namespace std; using namespace std::placeholders; class MockEventReceiver : public ILogLinesEvents { public: virtual ~MockEventReceiver() { CHECK(_expects.empty()); } void expect(const string& s) { _expects.push_back(s); } // an existing line was changed virtual void edit_line([[maybe_unused]] LogLines* ll, size_t pos, Line* line) { CHECK(pos == 10); CHECK(line->view() == "edit"); } // a new line was added virtual void append_line([[maybe_unused]] LogLines* ll, [[maybe_unused]] size_t pos, Line* line) { CHECK(!_expects.empty()); CHECK(line->view() == _expects.front()); _expects.pop_front(); } // an insertion event virtual void insertion([[maybe_unused]] LogLines* ll, [[maybe_unused]] size_t pos, [[maybe_unused]] size_t amount) {} // a deletion event virtual void deletion([[maybe_unused]] LogLines* ll, [[maybe_unused]] size_t pos, [[maybe_unused]] size_t amount) {} // clearing event virtual void clear_lines([[maybe_unused]] LogLines* ll) {} protected: list _expects; }; TEST_CASE("eventbus enroll") { MockEventReceiver m, m_other; LogLines* ll = (LogLines*) 12; LogLines* ll_other = (LogLines*) 13; EventBus::_()->enregister(ll, &m); EventBus::_()->enregister(ll_other, &m_other); m.expect("hello"); m.expect("there"); m_other.expect("another"); Line l1(string("hello")); Line l2(string("another")); Line l3(string("there")); Line l4(string("world")); Line l5(string("ignore")); EventBus::_()->append_line(ll, 0, &l1); EventBus::_()->append_line(ll_other, 1, &l2); EventBus::_()->append_line(ll, 1, &l3); m.expect("world"); EventBus::_()->append_line(ll, 2, &l4); EventBus::_()->deregister(ll, &m); EventBus::_()->append_line(ll, 3, &l5); EventBus::_()->deregister(ll_other, &m_other); } TEST_CASE("eventbus eventmaker end") { MockEventReceiver m; LogLines* ll = (LogLines*) 42; EventBus::_()->enregister(ll, &m); m.expect("hello"); m.expect("there"); Line l1(string("hello")); Line l2(string("there")); Line l3(string("world")); EventBus::_()->append_line(ll, 3, &l1); EventBus::_()->append_line(ll, 4, &l2); EventBus::_()->eventmaker_finished(ll); EventBus::_()->append_line(ll, 5, &l3); EventBus::_()->deregister(ll, &m); } TEST_CASE("eventbus eventmaker end and deregister") { MockEventReceiver m; LogLines* ll = (LogLines*) 42; EventBus::_()->enregister(ll, &m); m.expect("hello"); m.expect("there"); Line l1(string("hello")); Line l2(string("there")); Line l3(string("world")); EventBus::_()->append_line(ll, 3, &l1); EventBus::_()->append_line(ll, 4, &l2); EventBus::_()->eventmaker_finished(ll); EventBus::_()->append_line(ll, 5, &l3); EventBus::_()->deregister(ll, &m); } TEST_CASE("eventbus deregister and eventmaker end") { MockEventReceiver m; LogLines* ll = (LogLines*) 42; EventBus::_()->enregister(ll, &m); m.expect("hello"); m.expect("there"); Line l1(string("hello")); Line l2(string("there")); Line l3(string("world")); EventBus::_()->append_line(ll, 3, &l1); EventBus::_()->append_line(ll, 4, &l2); EventBus::_()->deregister(ll, &m); EventBus::_()->append_line(ll, 4, &l3); EventBus::_()->eventmaker_finished(ll); EventBus::_()->deregister(ll, &m); } TEST_CASE("eventbus multiple enroll") { MockEventReceiver m1; MockEventReceiver m2; MockEventReceiver m3; LogLines* s1 = (LogLines*) 42; LogLines* s2 = (LogLines*) 44; CHECK(!EventBus::_()->present(&m1)); EventBus::_()->enregister(s1, &m1); CHECK(EventBus::_()->present(&m1)); EventBus::_()->enregister(s1, &m2); EventBus::_()->enregister(s1, &m3); EventBus::_()->enregister(s2, &m2); EventBus::_()->enregister(s2, &m3); m1.expect("s1_1"); m1.expect("s1_2"); m1.expect("s1_3"); m2.expect("s1_1"); m2.expect("s2_1"); m2.expect("s1_2"); m2.expect("s1_3"); m2.expect("s2_2"); m3.expect("s1_1"); m3.expect("s2_1"); m3.expect("s1_2"); m3.expect("s1_3"); m3.expect("s2_2"); Line l1(string("s1_1")); Line l2(string("s2_1")); Line l3(string("s1_2")); Line l4(string("edit")); Line l5(string("s1_3")); Line l6(string("s2_2")); EventBus::_()->append_line(s1, 2, &l1); EventBus::_()->append_line(s2, 3, &l2); EventBus::_()->append_line(s1, 3, &l3); EventBus::_()->edit_line(s1, 10, &l4); EventBus::_()->append_line(s1, 4, &l5); EventBus::_()->append_line(s2, 4, &l6); EventBus::_()->deregister(s1, &m1); CHECK(!EventBus::_()->present(&m1)); CHECK(EventBus::_()->present(&m2)); EventBus::_()->deregister(s1, &m2); EventBus::_()->deregister(s1, &m3); EventBus::_()->deregister(s2, &m2); EventBus::_()->deregister(s2, &m3); } logserver/tests/test_exit_signal.cc000066400000000000000000000003021512456611200201020ustar00rootroot00000000000000#include "test.h" #include "exit_signal.h" TEST_CASE("check") { CHECK(ExitSignal::check(false) == false); CHECK(ExitSignal::check(true) == true); CHECK(ExitSignal::check(false) == true); } logserver/tests/test_explored_range.cc000066400000000000000000000176671512456611200206200ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test.h" #include #include "explored_range.h" using namespace std; set setify(size_t start, size_t end) { if (end < start) { size_t swp = end; end = start; start = swp; } set ret; for (size_t i = start; i < end; ++i) { ret.insert(i); } return ret; } void check_complete(const ExploredRange& range, size_t len) { set lines; range.disjunctive_join(&lines); REQUIRE(lines.size() == len); for (size_t i = 0; i < len; ++i) { CHECK(lines.count(i)); } } TEST_CASE("exploreange random whence") { mt19937 rng(random_device{}()); uniform_int_distribution range_dist(1, 10); uniform_int_distribution length_dist(1, 20); uniform_int_distribution coin_dist(0, 1); for (int i = 0; i < 100; ++i) { size_t len = length_dist(rng); len *= len; uniform_int_distribution whence_dist(0, len); size_t count = 0; size_t start = G::NO_POS; size_t end = G::NO_POS; size_t search = range_dist(rng); ExploredRange range(search); size_t whence = 0; range.mark_end(len); while (!range.completed()) { if (coin_dist(rng)) whence = whence_dist(rng); ++count; range.explore(whence, &start, &end); CHECK(start != G::NO_POS); CHECK(end != G::NO_POS); CHECK(start <= len); CHECK(end <= len); if (start != end) range.extend(start, end, setify(start, end)); } check_complete(range, len); } } TEST_CASE("exploreange top whence") { mt19937 rng(random_device{}()); uniform_int_distribution range_dist(1, 10); uniform_int_distribution length_dist(1, 20); for (int i = 0; i < 100; ++i) { size_t len = length_dist(rng); len *= len; size_t count = 0; size_t start = G::NO_POS; size_t end = G::NO_POS; size_t search = range_dist(rng); ExploredRange range(search); size_t whence = 0; range.mark_end(len); while (!range.completed()) { ++count; range.explore(whence, &start, &end); CHECK(start != G::NO_POS); CHECK(end != G::NO_POS); CHECK(start <= len + search); CHECK(end <= len + search); if (end > len) end = len; if (start != end) range.extend(start, end, setify(start, end)); } check_complete(range, len); } } TEST_CASE("exploreange bottom whence") { mt19937 rng(random_device{}()); uniform_int_distribution range_dist(1, 10); uniform_int_distribution length_dist(1, 20); for (int i = 0; i < 100; ++i) { size_t len = length_dist(rng); len *= len; size_t count = 0; size_t start = G::NO_POS; size_t end = G::NO_POS; size_t search = range_dist(rng); ExploredRange range(search); size_t whence = len; range.mark_end(len); while (!range.completed()) { ++count; range.explore(whence, &start, &end); CHECK(start != G::NO_POS); CHECK(end != G::NO_POS); CHECK(start <= len + search); CHECK(end <= len + search); if (end > len) end = len; if (start != end) range.extend(start, end, setify(start, end)); } check_complete(range, len); } } TEST_CASE("explore range insert lines") { ExploredRange range(5); size_t start, end; range.explore(0, &start, &end); CHECK(start == 0); CHECK(end == 5); set expected = {2}; range.extend(start, end, expected); set findings; range.disjunctive_join(&findings); CHECK(findings == expected); range.insert_or_delete_lines(3, 10, true); findings.clear(); range.disjunctive_join(&findings); CHECK(findings == expected); range.insert_or_delete_lines(1, 5, true); findings.clear(); range.disjunctive_join(&findings); expected = {7}; CHECK(findings == expected); } TEST_CASE("explore range gapped") { ExploredRange range(10); range.mark_end(30); size_t start, end; range.explore(0, &start, &end); CHECK(start == 0); CHECK(end == 10); set expected = {2, 5, 7}; range.extend(start, end, expected); range.explore(30, &start, &end); CHECK(end == 20); CHECK(start == 30); expected = {22, 26, 29}; range.extend(start, end, expected); CHECK(range.next_match(0, G::DIR_DOWN) == 2); CHECK(range.next_match(0, G::DIR_UP) == G::NO_POS); CHECK(range.next_match(3, G::DIR_DOWN) == 5); CHECK(range.next_match(3, G::DIR_UP) == 2); CHECK(range.next_match(8, G::DIR_DOWN) == G::NO_POS); CHECK(range.next_match(8, G::DIR_UP) == 7); CHECK(range.next_match(15, G::DIR_DOWN) == G::NO_POS); CHECK(range.next_match(15, G::DIR_UP) == G::NO_POS); CHECK(range.next_match(20, G::DIR_DOWN) == 22); CHECK(range.next_match(20, G::DIR_UP) == G::NO_POS); CHECK(range.next_match(24, G::DIR_DOWN) == 26); CHECK(range.next_match(24, G::DIR_UP) == 22); range.insert_or_delete_lines(6, 10, true); set findings; range.disjunctive_join(&findings); expected = {2, 5, 17, 32, 36, 39}; CHECK(findings == expected); } TEST_CASE("explore range next range") { ExploredRange range(10); range.mark_end(30); size_t start, end; range.explore(5, &start, &end); CHECK(start == 5); CHECK(end == 15); range.extend(start, end, set()); range.explore(28, &start, &end); CHECK(end == 18); CHECK(start == 28); range.extend(start, end, set()); CHECK(range.next_range(0, G::DIR_DOWN) == G::NO_POS); CHECK(range.next_range(0, G::DIR_UP) == G::NO_POS); CHECK(range.next_range(3, G::DIR_DOWN) == G::NO_POS); CHECK(range.next_range(3, G::DIR_UP) == G::NO_POS); CHECK(range.next_range(5, G::DIR_DOWN) == 15); CHECK(range.next_range(5, G::DIR_UP) == 5); CHECK(range.next_range(15, G::DIR_DOWN) == 15); CHECK(range.next_range(15, G::DIR_UP) == 5); CHECK(range.next_range(10, G::DIR_DOWN) == 15); CHECK(range.next_range(10, G::DIR_UP) == 5); CHECK(range.next_range(17, G::DIR_DOWN) == G::NO_POS); CHECK(range.next_range(17, G::DIR_UP) == G::NO_POS); CHECK(range.next_range(22, G::DIR_DOWN) == 28); CHECK(range.next_range(22, G::DIR_UP) == 18); } TEST_CASE("explored insert nogap") { ExploredRange range(10); range.mark_end(30); size_t start, end; range.explore(30, &start, &end); CHECK(start == 30); CHECK(end == 20); set expected = {22, 27}; range.extend(start, end, expected); range.post_end(33); range.post_end(38); CHECK(range.next_range(40, G::DIR_DOWN) == G::EOF_POS); CHECK(range.next_range(40, G::DIR_UP) == 20); CHECK(range.next_match(35, G::DIR_DOWN) == 38); CHECK(range.next_match(35, G::DIR_UP) == 33); CHECK(range.next_match(31, G::DIR_DOWN) == 33); CHECK(range.next_match(31, G::DIR_UP) == 27); CHECK(range.next_match(29, G::DIR_DOWN) == 33); CHECK(range.next_match(29, G::DIR_UP) == 27); CHECK(range.next_range(22, G::DIR_DOWN) == G::EOF_POS); CHECK(range.next_range(22, G::DIR_UP) == 20); } TEST_CASE("explored insert gap") { ExploredRange range(10); range.mark_end(30); size_t start, end; range.explore(17, &start, &end); CHECK(start == 17); CHECK(end == 27); set expected = {22, 25}; range.extend(start, end, expected); range.post_end(33); range.post_end(38); CHECK(range.next_range(40, G::DIR_DOWN) == G::EOF_POS); CHECK(range.next_range(40, G::DIR_UP) == 30); CHECK(range.next_match(35, G::DIR_DOWN) == 38); CHECK(range.next_match(35, G::DIR_UP) == 33); CHECK(range.next_match(31, G::DIR_DOWN) == 33); CHECK(range.next_match(31, G::DIR_UP) == G::NO_POS); CHECK(range.next_match(29, G::DIR_DOWN) == G::NO_POS); CHECK(range.next_match(29, G::DIR_UP) == G::NO_POS); CHECK(range.next_range(22, G::DIR_DOWN) == 27); CHECK(range.next_range(22, G::DIR_UP) == 17); } logserver/tests/test_fd_line_provider.cc000066400000000000000000000015671512456611200211240ustar00rootroot00000000000000#include "test.h" #include #include "fd_line_provider.h" #include "mock_log_lines.h" TEST_CASE("read fd") { int fd[2]; CHECK(!pipe(fd)); list data = { "hello", "there", "good", "people", "and", "welcome", "to", "the", "test"}; MockLogLines ll; ll.expect(data); FDLineProvider fdlp(&ll, fd[0]); fdlp.start(); for (const auto& x : data) { CHECK(x.size() == static_cast( write(fd[1], x.data(), x.size()))); CHECK(1 == write(fd[1], "\n", 1)); } while (!ll.no_expectations()) { usleep(1000); } CHECK(ll.no_expectations()); ll.expect("more data"); ll.expect("and yet more"); CHECK(10 == write(fd[1], "more data\n", 10)); usleep(10000); CHECK(13 == write(fd[1], "and yet more\n", 13)); while (!ll.no_expectations()) { usleep(1000); } // pipe is open CHECK(ll.eof() == false); close(fd[1]); while (!ll.eof()) usleep(100); } logserver/tests/test_file_line_provider.cc000066400000000000000000000027361512456611200214510ustar00rootroot00000000000000#include "test.h" #include #include "contrib/ozstream.hpp" #include "file_line_provider.h" #include "mock_log_lines.h" TEST_CASE("readfile") { list data = { "hello", "there", "good", "people", "and", "welcome", "to", "the", "test"}; filesystem::path filename = filesystem::weakly_canonical("/tmp/.logserver_filelinetest.dat"); ofstream fout(filename); CHECK(fout.good()); for (const auto &x : data) fout << x << endl; fout.flush(); MockLogLines ll; ll.expect(data); FileLineProvider flp(&ll, filename); flp.start(); while (!ll.eof()) { usleep(1000); } CHECK(ll.no_expectations()); ll.expect("more data"); ll.expect("and yet more"); fout << "more data" << endl; fout.flush(); usleep(10000); fout << "and yet more" << endl; fout.flush(); fout.close(); ifstream fin(filename); assert(fin.good()); fin.close(); while (!ll.no_expectations()) { usleep(1000); } // live updated file should no longer signal eof CHECK(ll.eof() == false); } TEST_CASE("read zip file") { list data = { "hello", "there", "good", "people", "and", "welcome", "to", "the", "test"}; filesystem::path filename = filesystem::weakly_canonical("/tmp/.logserver_filelinetest.dat"); ofstream fout(filename); zstream::ogzstream zout(fout); for (const auto &x : data) zout << x << endl; zout.close(); MockLogLines ll; ll.expect(data); FileLineProvider flp(&ll, filename); flp.start(); while (!ll.eof()) { usleep(1000); } CHECK(ll.no_expectations()); } logserver/tests/test_format_string.cc000066400000000000000000000057231512456611200204660ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test.h" #include "format_string.h" TEST_CASE("fs no format") { FormatString fs; fs.init("hello"); CHECK(fs == "hello"); for (size_t i = 0; i < 5; ++i) { CHECK(fs.code(i) == 0); } } TEST_CASE("fs append") { FormatString fs; fs.init("hello "); fs.add("world", 6); CHECK(fs == "hello world"); for (size_t i = 0; i < 11; ++i) { CHECK(fs.code(i) == (i >= 6 ? 6 : 0)); } } TEST_CASE("fs multi append") { FormatString fs; fs.init("hello "); fs.add("there ", 2); fs.add("world", 6); CHECK(fs == "hello there world"); for (size_t i = 0; i < 17; ++i) { if (i >= 12) CHECK(fs.code(i) == 6); else if (i >= 6) CHECK(fs.code(i) == 2); else CHECK(fs.code(i) == 0); } } TEST_CASE("fs cursors") { FormatString fs; fs.init("hello"); fs.cursor(0); CHECK(fs.code(0) == G::CURSOR_COLOUR); CHECK(fs.code(1) == 0); } TEST_CASE("fs highlight") { FormatString fs; fs.init("hello "); fs.add("world", 6); fs.highlight(); CHECK(fs == "hello world"); for (size_t i = 0; i < 11; ++i) { CHECK(fs.code(i) == (i >= 6 ? 38 : 32)); } } TEST_CASE("fs keyword") { FormatString fs; fs.init("hello there world"); fs.mark(1, "hello"); for (size_t i = 0; i < 11; ++i) { CHECK(fs.code(i) == (i >= 5 ? 0 : 1)); } } TEST_CASE("fs multikeyword") { FormatString fs; fs.init("hello there world"); fs.mark(1, "hello"); fs.mark(3, "there"); for (size_t i = 0; i < 11; ++i) { if (i < 5) CHECK(fs.code(i) == 1); else if (i >= 6 && i < 11) CHECK(fs.code(i) == 3); else CHECK(fs.code(i) == 0); } } TEST_CASE("fs colon") { FormatString fs; fs.init("some key: value"); fs.colour_function(); for (size_t i = 0; i < 14; ++i) { if (i >= 4 && i < 9) { CHECK(fs.code(i) == G::COLON_COLOUR); } else CHECK(fs.code(i) == 0); } } TEST_CASE("fs add pair") { FormatString fs; fs.init("hello"); fs.add("world", "\5\4\3\2\1"); for (size_t i = 0; i < 5; ++i) { CHECK(fs.code(i) == 0); } for (int i = 5; i < 10; ++i) { CHECK(fs.code(i) == 10 - i); } CHECK((string) fs == "helloworld"); } TEST_CASE("fs add fs") { FormatString fs1, fs2, fs; fs1.add("hello", 3); fs2.add("world", 4); fs.add(fs1); fs.add(fs2); for (size_t i = 0; i < 5; ++i) { CHECK(fs.code(i) == 3); } for (size_t i = 5; i < 10; ++i) { CHECK(fs.code(i) == 4); } CHECK((string) fs == "helloworld"); } logserver/tests/test_g.cc000066400000000000000000000010111512456611200160200ustar00rootroot00000000000000#include "test.h" #include "constants.h" TEST_CASE("lower already") { CHECK(G::to_lower("") == ""); CHECK(G::to_lower("a") == "a"); CHECK(G::to_lower("abc") == "abc"); } TEST_CASE("all upper") { CHECK(G::to_lower("A") == "a"); CHECK(G::to_lower("ABC") == "abc"); } TEST_CASE("some upper") { CHECK(G::to_lower("Ab") == "ab"); CHECK(G::to_lower("aB") == "ab"); CHECK(G::to_lower("AbC") == "abc"); CHECK(G::to_lower("abC") == "abc"); CHECK(G::to_lower("Abc") == "abc"); CHECK(G::to_lower("aBc") == "abc"); } logserver/tests/test_huge_vector.cc000066400000000000000000000131301512456611200201110ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include "test.h" #include "huge_vector.h" using namespace std; class string_holder { public: string_holder(const string& init) : _s(init) {} inline const string& get() const { return _s; } string_view view() const { return _s; } protected: string _s; }; /* huge_vector should function like a normal vector, so we can just randomly do * ops and make sure they are equal */ TEST_CASE("random use") { mt19937 rng(random_device{}()); uniform_int_distribution action(0, 9); uniform_int_distribution chardist('a', 'z'); for (int i = 0; i < 10; ++i) { huge_vector, 64> test; vector _control; uniform_int_distribution event_dist(10, 1000); int total_events = event_dist(rng); for (int j = 0; j < total_events; ++j) { test.sanity(); int cur = action(rng); CHECK(test.length() == _control.size()); if (cur < 6 || !_control.size()) { string s = ""; s += chardist(rng); s += chardist(rng); s += chardist(rng); _control.push_back(s); test.add(make_unique(s)); } else if (cur < 8) { uniform_int_distribution pos_dist(0, _control.size() - 1); size_t pos = pos_dist(rng); _control.erase(_control.begin() + pos); test.remove(pos); } else if (cur == 8) { uniform_int_distribution pos_dist(0, _control.size() - 1); size_t amount = action(rng); size_t pos = pos_dist(rng); list to_add; list> to_add_test; for (size_t subpos = 0; subpos < amount; ++subpos) { string s = ""; s += chardist(rng); s += chardist(rng); to_add.push_back(s); to_add_test.push_back(make_unique(s)); } _control.insert(_control.begin() + pos, to_add.begin(), to_add.end()); test.insert(to_add_test, pos); } else { uniform_int_distribution pos_dist(0, _control.size() - 1); size_t pos = pos_dist(rng); string s = ""; s += chardist(rng); s += chardist(rng); _control.insert(_control.begin() + pos, s); test.insert(make_unique(s), pos); } for (size_t pos = 0; pos < _control.size(); ++pos) { CHECK(test.valid(pos)); CHECK(_control[pos] == test[pos]->get()); CHECK(test[pos] == test.at(pos)); } for (size_t pos = 0; pos < 100; ++pos) { CHECK(!test.valid(_control.size() + pos)); CHECK(!test.valid(_control.size() + pos + 10000)); } } } } TEST_CASE("iterate empty") { huge_vector, 2> test; for (const auto& x : test) { CHECK(x != x); } } TEST_CASE("iterate") { huge_vector, 2> test; test.add(make_unique("hello")); test.add(make_unique("there")); test.add(make_unique("world")); test.add(make_unique("and")); test.add(make_unique("goodday")); test.add(make_unique("as")); test.add(make_unique("well")); test.add(make_unique("as")); test.add(make_unique("farewell")); list results = {"hello", "there", "world", "and", "goodday", "as", "well", "as", "farewell"}; size_t i = 0; pagepos_t pp; pp.page = 0; pp.off = 0; auto it = test.begin(); for (const auto& x : test) { CHECK(test.valid(pp)); CHECK(test.valid(i)); CHECK(x == *it); CHECK(x == test[i]); CHECK(x->get() == results.front()); results.pop_front(); test.next(&pp); ++i; ++it; } CHECK(results.empty()); CHECK(!test.valid(pp)); CHECK(!test.valid(i)); CHECK(it == test.end()); } TEST_CASE("clear") { huge_vector, 2> test; test.add(make_unique("hello")); test.add(make_unique("there")); test.add(make_unique("world")); test.add(make_unique("and")); test.clear(); CHECK(test.length() == 0); test.add(make_unique("goodday")); CHECK(test[0]->get() == "goodday"); } TEST_CASE("clear single") { huge_vector, 8> test; test.add(make_unique("hello")); test.add(make_unique("there")); test.add(make_unique("world")); test.add(make_unique("and")); test.clear(); CHECK(test.length() == 0); test.add(make_unique("goodday")); CHECK(test[0]->get() == "goodday"); } TEST_CASE("write") { huge_vector, 8> test; test.add(make_unique("hello")); test.add(make_unique("there")); test.add(make_unique("world")); test.add(make_unique("and")); test.add(make_unique("goodday")); stringstream ss; test.write(ss); CHECK(ss.str() == "hello\nthere\nworld\nand\ngoodday\n"); } logserver/tests/test_line.cc000066400000000000000000000112411512456611200165270ustar00rootroot00000000000000#include "test.h" #include "base_line_provider.h" #include "line.h" TEST_CASE("store and fetch") { Line l(string("hello")); CHECK(l.length() == 5); CHECK(l.view() == l.get()); CHECK(l.view() == "hello"); } TEST_CASE("update and revert") { Line l(string("hello")); l.set("updated"); CHECK(l.get() == "updated"); CHECK(l.view() == "updated"); CHECK(l.length() == 7); l.revert(); CHECK(l.get() == "hello"); CHECK(l.view() == "hello"); CHECK(l.length() == 5); } TEST_CASE("double update and revert") { Line l(string("hello")); l.set("updated"); CHECK(l.get() == "updated"); CHECK(l.view() == "updated"); CHECK(l.length() == 7); l.set("updated again"); CHECK(l.get() == "updated again"); CHECK(l.view() == "updated again"); CHECK(l.length() == 13); l.revert(); CHECK(l.get() == "hello"); CHECK(l.view() == "hello"); CHECK(l.length() == 5); } TEST_CASE("ansi") { string s = "\x1b[4mhello\x1b[24m"; auto x = BaseLineProvider::get_string_and_format(s); CHECK(x.first == "hello"); CHECK(x.second == "@@@@@"); } TEST_CASE("ansi colour") { string s = "\x1b[34mhello\x1b[37mthere"; auto x = BaseLineProvider::get_string_and_format(s); CHECK(x.first == "hellothere"); CHECK(x.second == string("\4\4\4\4\4\0\0\0\0\0", 10)); } TEST_CASE("ansi colour and bold") { string s = "\x1b[34mhello\x1b[4mthere"; auto x = BaseLineProvider::get_string_and_format(s); CHECK(x.first == "hellothere"); CHECK(x.second == "\4\4\4\4\4DDDDD"); } /* check conversion from string and format, to line, to formatstring */ void check_line_to_formatline(const string& str, const string& fmt) { // this test won't work if there is a tab in string, because the // formatstring will expand it. make another test for that CHECK(str.find('\t') == string::npos); Line line(str, fmt); FormatString fs; size_t ret = line.render(100, true, nullopt, 0, set(), nullptr, &fs); CHECK(ret == fs.size()); CHECK(fmt.size() == fs.size()); CHECK(fmt.size() == str.size()); for (size_t i = 0; i < str.size(); ++i) { CHECK(fs.code(i) == (int) fmt[i]); } } TEST_CASE("ansi giterror") { string s = "\x1b\x5b\x33\x32\x6d\x2b\x1b\x5b\x6d\x09\x1b\x5b\x33\x32\x6d\x73"; s += "\x74\x72\x69\x6e\x67\x20\x73\x20\x3d\x20\x22\x5c\x78\x31\x62\x5b"; s += "\x33\x36\x6d\x40\x40\x20\x2d\x31\x35\x31"; auto x = BaseLineProvider::get_string_and_format(s); CHECK(x.second[0] == 2); Line line(x.first, x.second); FormatString fs; size_t ret = line.render(100, true, nullopt, 0, set(), nullptr, &fs); CHECK(ret == fs.size()); CHECK(fs.code(0) == 2); } TEST_CASE("ansi gitline") { string s = "\x1b[36m@@ -151,6 +152,7 @@ \x1b[m \x1b[mprotected: \x1b[m"; auto x = BaseLineProvider::get_string_and_format(s); CHECK(x.first == "@@ -151,6 +152,7 @@ protected: "); string fmt(x.first.length(), '\6'); CHECK(x.second.length() == x.first.length()); fmt[2] = '\0'; fmt[9] = '\0'; fmt[16] = '\0'; fmt[19] = '\0'; fmt[20] = '\0'; fmt[21] = '\0'; fmt[32] = '\0'; CHECK(x.second == fmt); check_line_to_formatline(x.first, x.second); s = "\x1b[32m+ \x1b[m \x1b[32mcolour \x1b[m"; x = BaseLineProvider::get_string_and_format(s); CHECK(x.first == "+ colour "); CHECK(x.second.length() == x.first.length()); fmt = string(x.first.length(), '\2'); fmt[1] = '\0'; fmt[2] = '\0'; fmt[9] = '\0'; CHECK(x.second == fmt); check_line_to_formatline(x.first, x.second); } TEST_CASE("no backspace") { string s = "hello"; auto x = BaseLineProvider::get_string_and_format(s); CHECK(x.first == s); CHECK(x.second == string("\0\0\0\0\0", 5)); } TEST_CASE("backspace underline") { string s = "he_\bl_\blo"; string f = string("\0\0\64\64\0", 5); auto x = BaseLineProvider::get_string_and_format(s); CHECK("hello" == x.first); CHECK(f == x.second); } TEST_CASE("backspace bold") { BaseLineProvider bp(nullptr); string s = "hel\bll\blo\bo"; string f = string("\0\0\32\32\32", 5); auto x = BaseLineProvider::get_string_and_format(s); CHECK("hello" == x.first); CHECK(f == x.second); } TEST_CASE("tabstops") { list l = {0, 4, 9}; Line line(string("this\tthat\twheeeee")); CHECK(l == line.tabstops('\t')); } TEST_CASE("tabstops csv") { list l = {0, 4, 9}; Line line(string("this,that,wheeeee")); CHECK(l == line.tabstops(',')); CHECK(list{0} == line.tabstops('\t')); } TEST_CASE("tabstops quote") { list l = {0, 4, 12}; Line line(string("this,\"th,at\",last")); CHECK(l == line.tabstops(',')); } TEST_CASE("tabstops quote escape") { TabData tab_data; list l = {0, 4, 18}; Line line(string("this,\"th,at\\\"more\",last")); CHECK(l == line.tabstops(',')); line.measure_tabs(',', &tab_data); CHECK(tab_data.width(0) == 5); CHECK(tab_data.width(1) == 15); CHECK(tab_data.width(2) == 0); } logserver/tests/test_line_filter_keyword.cc000066400000000000000000000141271512456611200216460ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test.h" #include #include "ncurses.h" #include "mock_log_lines_for_filter.h" #include "line_filter_keyword.h" using namespace std; TEST_CASE("line filter random") { mt19937 rng(random_device{}()); uniform_int_distribution length_dist(1, 25); uniform_int_distribution percent_dist(0, 100); uniform_int_distribution<> invert_dist(0, 1); uniform_int_distribution anchor_dist(0, 2); for (int j = 0; j < 10; ++j) { set all; set matches; size_t len = length_dist(rng); int anchor = anchor_dist(rng); int parms = Match::PRESENT | Match::ANCHOR_NONE; bool not_inverted = invert_dist(rng); if (!not_inverted) parms |= Match::MISSING; if (anchor == 1) parms |= Match::ANCHOR_LEFT; if (anchor == 2) parms |= Match::ANCHOR_RIGHT; len *= len; len *= len; string keyword = "some match"; size_t percent = percent_dist(rng); MockLogLinesForFilter ll(len, parms, keyword); for (size_t i = 0; i < len; ++i) { all.insert(i); if (percent_dist(rng) < percent) { ll.expect_match(i); if (not_inverted) matches.insert(i); } else if (!not_inverted) matches.insert(i); } LineFilterKeyword lfk(ll, parms, 0); lfk.current_type(keyword); lfk.finish(); CHECK(lfk.get_keyword() == keyword); CHECK(!lfk.empty()); while (!ll.checked_all()) usleep(1000); for (size_t i = 0; i < len; ++i) { if (matches.count(i)) CHECK(lfk.is_match(i)); else CHECK(!lfk.is_match(i)); } set disjunction; // empty set gets all matches lfk.disjunctive_join(&disjunction); CHECK(disjunction == matches); // full set becomes all matches lfk.conjunctive_join(&all); CHECK(all == matches); // all matches stays the same lfk.conjunctive_join(&all); CHECK(all == matches); } } void check_num_matches_eq(const LineFilterKeyword& lfk, size_t num) { set disjunction; lfk.disjunctive_join(&disjunction); CHECK(disjunction.size() == num); } void check_num_matches_le(const LineFilterKeyword& lfk, size_t num) { set disjunction; lfk.disjunctive_join(&disjunction); CHECK(disjunction.size() <= num); } void sleep_until_checked(const MockLogLinesForFilter& ll, size_t matched, const LineFilterKeyword& lfk, size_t num) { while (!ll.checked_number(matched)) { check_num_matches_le(lfk, num); usleep(100); } } TEST_CASE("line filter keystroke following") { mt19937 rng(random_device{}()); MockLogLinesForFilter ll( 5000, 0); ll.expect("va", 10); ll.expect("a", 10); ll.expect("ab", 10); ll.expect("abc", 10); ll.expect("abcd", 10); LineFilterKeyword lfk(ll, Match::PRESENT, 0); lfk.current_type(""); usleep(1000); CHECK(ll.checked_number(0)); lfk.current_type("a"); sleep_until_checked(ll, 5000, lfk, 50); check_num_matches_eq(lfk, 50); lfk.current_type("va"); sleep_until_checked(ll, 10000, lfk, 10); check_num_matches_eq(lfk, 10); lfk.current_type("a"); sleep_until_checked(ll, 15000, lfk, 50); check_num_matches_eq(lfk, 50); lfk.current_type("ab"); sleep_until_checked(ll, 20000, lfk, 30); check_num_matches_eq(lfk, 30); lfk.current_type("abc"); sleep_until_checked(ll, 25000, lfk, 20); check_num_matches_eq(lfk, 20); lfk.current_type("abcd"); sleep_until_checked(ll, 30000, lfk, 10); while (!ll.checked_number(30000)) usleep(1000); check_num_matches_eq(lfk, 10); lfk.finish(); check_num_matches_eq(lfk, 10); // checks that after finishing the last of .. means complete CHECK(lfk.get_description() == "abcd"); } /* Starts a search from the end position, checks that it always grows upwards * and has .. while the search is ongoing and results are available. */ TEST_CASE("line filter append") { mt19937 rng(random_device{}()); MockLogLinesForFilter ll( 100000, 0); ll.expect("abcd", 1000); LineFilterKeyword lfk(ll, Match::PRESENT, 0); lfk.current_type("abcd"); lfk.finish(); lfk.set_whence(G::NO_POS); lfk.post_end(110000); lfk.post_end(120000); lfk.post_end(130000); lfk.post_end(140000); size_t earliest = G::NO_POS; size_t last_size = 4; while (true) { string name = lfk.get_description(); set vals; lfk.disjunctive_join(&vals); CHECK(vals.size() >= last_size); CHECK(*vals.begin() <= earliest); if (*vals.begin() == earliest) { CHECK(vals.size() == last_size); } // we are done searching if (vals.size() == 1004) { CHECK(name == "abcd"); break; } // we are still seaching CHECK(name == "abcd.."); last_size = vals.size(); earliest = *vals.begin(); CHECK(lfk.next_match(earliest + 1, G::DIR_UP) != G::NO_POS); CHECK(lfk.next_match(earliest + 10, G::DIR_UP) != G::NO_POS); CHECK(lfk.next_match(earliest + 100, G::DIR_UP) != G::NO_POS); CHECK(lfk.next_match(earliest + 1000, G::DIR_UP) != G::NO_POS); CHECK(lfk.next_match(earliest + 10000, G::DIR_UP) != G::NO_POS); CHECK(lfk.next_match(earliest + 100000, G::DIR_UP) != G::NO_POS); CHECK(lfk.next_match(earliest + 10, G::DIR_DOWN) != G::NO_POS); CHECK(lfk.next_match(141000, G::DIR_UP) == 140000); CHECK(lfk.next_match(141000, G::DIR_DOWN) == G::NO_POS); usleep(100); } CHECK(ll.checked_number(100000)); } TEST_CASE("line filter string cast") { MockLogLinesForFilter ll( 5000, 0); LineFilterKeyword lfk(ll, Match::PRESENT, 0); CHECK(static_cast(lfk) == ""); lfk.current_type("abcd"); CHECK(static_cast(lfk) == "abcd"); lfk.finish(); CHECK(static_cast(lfk) == "abcd"); } logserver/tests/test_line_intelligence.cc000066400000000000000000000235051512456611200212570ustar00rootroot00000000000000#include "test.h" #include "line_intelligence.h" #include using namespace std; TEST_CASE("tabs") { CHECK(LineIntelligence::make_tabs(0) == ""); CHECK(LineIntelligence::make_tabs(1) == " "); CHECK(LineIntelligence::make_tabs(2) == " "); } TEST_CASE("code") { list out; list expected = { "func(){", " i;", " j;", " while(true){", " k;", " }", "}"}; string in = "func(){i;j;while(true){k;}}"; LineIntelligence::process_code(in, &out); CHECK(out == expected); } TEST_CASE("code quote") { list out; list expected = { "func(){", " i;", " j = \"}{{{\";", " while(true){", " k;", " }", "}"}; string in = "func(){i;j = \"}{{{\";while(true){k;}}"; LineIntelligence::process_code(in, &out); CHECK(out == expected); } TEST_CASE("json") { list out; list expected = { "{", " \"key\":\"value\",", " \"obj\":{", " \"a\":2,", " \"b\":3", " }", "}"}; string in = "{\"key\":\"value\", \"obj\":{\"a\":2, \"b\":3}}"; map counts; LineIntelligence::count_occurrences(in, &counts); CHECK(LineIntelligence::heuristic_json(in, counts)); CHECK(!LineIntelligence::heuristic_code(in, counts)); CHECK(!LineIntelligence::heuristic_newlines(in, counts)); CHECK(!LineIntelligence::heuristic_pipe(in, counts)); CHECK(!LineIntelligence::heuristic_httparg(in, counts)); LineIntelligence::process_json(in, &out); CHECK(out == expected); } TEST_CASE("json array") { list out; list expected = { "[", " 3,", " \"hello, there\",", " true", "]"}; string in = "[3, \"hello, there\", true]"; LineIntelligence::process_json(in, &out); CHECK(out == expected); } template bool equal(const T& lhs, const R& rhs) { auto lit = lhs.begin(); auto rit = rhs.begin(); while (lit != lhs.end()) { if (rit == rhs.end()) return false; if (*lit != *rit) return false; ++lit; ++rit; } if (rit != rhs.end()) return false; return true; } TEST_CASE("split with empty") { string data = "one,two,,four"; list expected = {"one", "two", "", "four"}; list tokens; LineIntelligence::split_with_empty(data, ",", &tokens); CHECK(equal(tokens, expected)); } TEST_CASE("split with empty end") { string data = "one,two,,"; list expected = {"one", "two", "", ""}; list tokens; LineIntelligence::split_with_empty(data, ",", &tokens); CHECK(equal(tokens, expected)); } TEST_CASE("split with empty start") { string data = "-one-two"; list expected = {"", "one", "two"}; list tokens; LineIntelligence::split_with_empty(data, "-", &tokens); CHECK(equal(tokens, expected)); } TEST_CASE("no split") { string data = "one-two"; list expected = {"one-two"}; list tokens; LineIntelligence::split_with_empty(data, ".", &tokens); CHECK(equal(tokens, expected)); } TEST_CASE("empty split") { string data = ""; list expected = {""}; list tokens; LineIntelligence::split_with_empty(data, ",", &tokens); CHECK(equal(tokens, expected)); } TEST_CASE("replace multi") { string data = "hello, there, world"; CHECK(LineIntelligence::replace(data, ", ", ".") == "hello.there.world"); } TEST_CASE("replace char") { string data = "hello there"; CHECK(LineIntelligence::replace(data, "t", ".") == "hello .here"); } TEST_CASE("replace none") { string data = "hello there"; CHECK(LineIntelligence::replace(data, "why", "what") == data); } TEST_CASE("rewind") { string data = "hello there wherecanwefind a good break?"; CHECK(LineIntelligence::rewind(data, 4, 2) == 4); CHECK(LineIntelligence::rewind(data, 7, 5) == 5); CHECK(LineIntelligence::rewind(data, 24, 2) == 24); CHECK(LineIntelligence::rewind(data, 24, 15) == 11); } TEST_CASE("rewind punctuation") { string data = "a=b&c=d"; CHECK(LineIntelligence::rewind(data, 2, 1) == 2); CHECK(LineIntelligence::rewind(data, 3, 2) == 3); CHECK(LineIntelligence::rewind(data, 4, 1) == 4); CHECK(LineIntelligence::rewind(data, 4, 2) == 3); CHECK(LineIntelligence::rewind(data, 4, 3) == 3); CHECK(LineIntelligence::rewind(data, 5, 1) == 5); CHECK(LineIntelligence::rewind(data, 5, 2) == 5); CHECK(LineIntelligence::rewind(data, 5, 3) == 3); } TEST_CASE("percent printable") { CHECK(LineIntelligence::percent_printable("abcd", false) == 100); CHECK(LineIntelligence::percent_printable("abcd okay", false) == 100); CHECK(LineIntelligence::percent_printable("", false) == 100); CHECK(LineIntelligence::percent_printable("ab\4\4", false) == 50); CHECK(LineIntelligence::percent_printable("ab\4\2ccdd", false) == 75); CHECK(LineIntelligence::percent_printable("\bcd\xff", false) == 50); } TEST_CASE("count occurences") { CHECK(LineIntelligence::count_occurrences("abcd", 'a') == 1); CHECK(LineIntelligence::count_occurrences("abba", 'a') == 2); CHECK(LineIntelligence::count_occurrences("ab....c.d", '.') == 5); CHECK(LineIntelligence::count_occurrences("abcd", '.') == 0); CHECK(LineIntelligence::count_occurrences("abcd", "a") == 1); CHECK(LineIntelligence::count_occurrences("abba", "a") == 2); CHECK(LineIntelligence::count_occurrences("abbab", "ab") == 2); CHECK(LineIntelligence::count_occurrences("ab....c.d", ".") == 5); CHECK(LineIntelligence::count_occurrences("abcd", ".") == 0); CHECK(LineIntelligence::count_occurrences("abcd", "abcd") == 1); CHECK(LineIntelligence::count_occurrences("ababababab", "abab") == 2); CHECK(LineIntelligence::count_occurrences("bbbaabbbabbb", "bbb") == 3); } TEST_CASE("heuristic timestamp") { string line = "the current time is 1762620592 seconds past epoch"; optional ret = LineIntelligence::apply_heuristics(line, 0); CHECK(ret); // in case timezone effect timestamp CHECK(ret->find(" 2025 seconds") != string::npos); } TEST_CASE("heuristic only timestamp") { string line = "1762620592"; optional ret = LineIntelligence::apply_heuristics(line, 0); CHECK(ret); // in case timezone effect timestamp CHECK(ret->ends_with(" 2025")); } TEST_CASE("heuristic only timestamp millis") { string line = "1762620592123"; optional ret = LineIntelligence::apply_heuristics(line, 0); CHECK(ret); // in case timezone effect timestamp CHECK(ret->ends_with(" 2025")); } TEST_CASE("base64") { string in = "dmVyeSBnb29kIHN0cmluZw=="; optional ret = LineIntelligence::apply_heuristics(in, 0); CHECK(ret); CHECK(*ret == "very good string"); in[9] = '+'; in[10] = '+'; in[11] = '+'; ret = LineIntelligence::apply_heuristics(in, 0); CHECK(!ret); ret = LineIntelligence::apply_heuristics(in, 1); CHECK(!ret); ret = LineIntelligence::apply_heuristics(in, 2); CHECK(ret); CHECK(ret->starts_with("very go")); CHECK(ret->ends_with(" string")); } TEST_CASE("subbase64") { string in = "parameter=dmVyeSBnb29kIHN0cmluZw=="; optional ret = LineIntelligence::apply_heuristics(in, 0); CHECK(ret); CHECK(*ret == "parameter=very good string"); } TEST_CASE("hex") { string in = "6f6b61792074686572652068656c6c6f"; optional ret = LineIntelligence::apply_heuristics(in, 0); CHECK(ret); CHECK(*ret == "okay there hello"); } TEST_CASE("hex split") { string in = "6f6b6179 and also 7468657265 are present"; optional ret = LineIntelligence::apply_heuristics(in, 0); CHECK(ret); CHECK(*ret == "okay and also there are present"); } TEST_CASE("apply matches") { string in = "here is a string with some matches some repeat"; map replace; CHECK(LineIntelligence::apply_matches(in, replace) == nullopt); replace["string"] = "sentence"; CHECK(*LineIntelligence::apply_matches(in, replace) == "here is a sentence with some matches some repeat"); replace.clear(); replace["some"] = "a bunch of"; replace["here"] = "this"; CHECK(*LineIntelligence::apply_matches(in, replace) == "this is a string with a bunch of matches a bunch of repeat"); } TEST_CASE("newline heuristic") { string in = "this line has a bunch\\nof newlines at various\\npositions " "along the string\\nwhich can be used to indicate\\nit should " "break at those positions."; map counts; LineIntelligence::count_occurrences(in, &counts); CHECK(!LineIntelligence::heuristic_json(in, counts)); CHECK(!LineIntelligence::heuristic_code(in, counts)); CHECK(!LineIntelligence::heuristic_httparg(in, counts)); CHECK(LineIntelligence::heuristic_newlines(in, counts)); auto lines = LineIntelligence::split(in, '\0'); CHECK(lines.size() == 5); CHECK(lines.front()->get() == "this line has a bunch"); CHECK(lines.back()->get() == "it should break at those positions."); } TEST_CASE("httparg heuristic") { string in = "query=string&formatted=data&x=3&y&enable&data=eyJ"; map counts; LineIntelligence::count_occurrences(in, &counts); CHECK(!LineIntelligence::heuristic_json(in, counts)); CHECK(!LineIntelligence::heuristic_code(in, counts)); CHECK(LineIntelligence::heuristic_httparg(in, counts)); CHECK(!LineIntelligence::heuristic_newlines(in, counts)); auto lines = LineIntelligence::split(in, '\0'); CHECK(lines.size() == 6); CHECK(lines.front()->get() == "query=string"); CHECK(lines.back()->get() == "data=eyJ"); } TEST_CASE("random data") { mt19937 rng(random_device{}()); uniform_int_distribution chardist(33, 126); uniform_int_distribution lendist(1, 1000); /* throw random strings at line intelligence to make sure it doesn't * crash on bad input */ for (int i = 0; i < 1000; ++i) { string s; size_t len = lendist(rng); while (len) { s += chardist(rng); --len; } optional ret = LineIntelligence::apply_heuristics(s, 0); } } TEST_CASE("percent encoding") { CHECK(LineIntelligence::hex_unescape("%2F") == "/"); CHECK(LineIntelligence::hex_unescape("%2F ") == "/ "); CHECK(LineIntelligence::hex_unescape(" %2F") == " /"); } logserver/tests/test_match.cc000066400000000000000000000036051512456611200167010ustar00rootroot00000000000000#include "test.h" #include "match.h" TEST_CASE("commit match present") { Match m(Match::ANCHOR_NONE); Match ml(Match::ANCHOR_LEFT); Match mr(Match::ANCHOR_RIGHT); m.commit_keyword("hello"); // start CHECK(m.is_match("hello there!")); // middle m.commit_keyword("lo th"); CHECK(m.is_match("hello there!")); // multi m.commit_keyword("e"); CHECK(m.is_match("hello there!")); // end m.commit_keyword("there!"); CHECK(m.is_match("hello there!")); // anchor present ml.commit_keyword("hello"); CHECK(ml.is_match("hello there!")); mr.commit_keyword("here!"); CHECK(mr.is_match("hello there!")); // in middle ml.commit_keyword("lo"); mr.commit_keyword("lo"); CHECK(!ml.is_match("hello there!")); CHECK(!mr.is_match("hello there!")); // at other end ml.commit_keyword("there!"); mr.commit_keyword("hello"); CHECK(!ml.is_match("hello there!")); CHECK(!mr.is_match("hello there!")); } TEST_CASE("commit case sensitivity") { Match m(0); Match ml(Match::ANCHOR_LEFT); Match mr(Match::ANCHOR_RIGHT); m.commit_keyword("heLLo"); CHECK(m.is_match("hi heLLo there!")); m.commit_keyword("HELLO"); CHECK(!m.is_match("hi heLLo there!")); m = Match(0); m.commit_keyword("hello"); CHECK(m.is_match("hi HeLLo there!")); CHECK(m.is_match("hi HELLO there!")); m.commit_keyword("Hello"); CHECK(!m.is_match("hi HELLO there!")); CHECK(m.is_match("hi Hello there!")); ml.commit_keyword("Hello"); mr.commit_keyword("Hello"); CHECK(ml.is_match("Hello there!")); CHECK(!ml.is_match("HellO there!")); CHECK(mr.is_match("a big Hello")); CHECK(!mr.is_match("a big HellO")); } TEST_CASE("commit match not present") { Match m(Match::ANCHOR_NONE); Match ml(Match::ANCHOR_LEFT); Match mr(Match::ANCHOR_RIGHT); m.commit_keyword("hello"); ml.commit_keyword("hello"); mr.commit_keyword("hello"); CHECK(!m.is_match("hi there!")); CHECK(!ml.is_match("hi there!")); CHECK(!mr.is_match("hi there!")); } logserver/tests/test_navigation.cc000066400000000000000000000103611512456611200177410ustar00rootroot00000000000000#include "test.h" #include using namespace std; #include "navigation.h" TEST_CASE("basic setup") { Navigation navi; CHECK(!navi.at_end()); CHECK(navi.at_line_start()); CHECK(navi.cur() == 0); Navigation copy(navi); CHECK(!navi.at_end()); CHECK(navi.at_line_start()); CHECK(navi.cur() == 0); } TEST_CASE("move above 0") { Navigation navi; CHECK(navi.cur() == 0); navi.up(); CHECK(navi.cur() == 0); navi.page_up(); CHECK(navi.cur() == 0); } TEST_CASE("goto tab") { Navigation navi; navi.goto_pos(0); CHECK(navi.tab() == 0); CHECK(navi.at_line_start()); navi.goto_pos(10); CHECK(navi.tab() == 0); navi.goto_pos(15); CHECK(navi.tab() == 0); navi.goto_pos(30); CHECK(navi.tab() == 0); CHECK(navi.at_line_start()); navi.goto_pos(40); CHECK(navi.tab() == 40); CHECK(!navi.at_line_start()); navi.goto_pos(60); CHECK(navi.tab() == 40); navi.goto_pos(90); CHECK(navi.tab() == 80); } TEST_CASE("goto and jump") { Navigation navi; navi.goto_line(10, false); CHECK(navi.cur() == 10); navi.goto_line(20, true); CHECK(navi.cur() == 20); navi.jump_back(); navi.goto_line(30, true); CHECK(navi.cur() == 30); navi.jump_back(); CHECK(navi.cur() == 10); navi.jump_back(); CHECK(navi.cur() == 30); navi.end(); CHECK(navi.cur() == G::NO_POS); CHECK(navi.at_end()); } size_t nice(size_t pos) { if (pos == G::NO_POS) return 0; return pos; } void set_view(Navigation* navi, const set& full) { size_t cur = navi->cur(); vector view; view.resize(51, G::NO_POS); auto it_down = full.lower_bound(cur); auto it_up = it_down; size_t p = 24; while (it_up != full.begin()) { --it_up; view[p] = *it_up; if (p == 0) break; --p; } for (size_t i = 25; i < 51; ++i) { if (it_down != full.end()) view[i] = *it_down; else break; ++it_down; } /* trace array for (size_t i = 0; i < 51 ; ++i) { cout << i << ": " << nice(view[i]) << " "; } cout << endl;*/ navi->set_view(view); } TEST_CASE("move around") { mt19937 rng(random_device{}()); for (int i = 0; i < 1000; ++i) { unique_ptr navi(new Navigation()); set fullview; uniform_int_distribution move_dist(0, 6); uniform_int_distribution len_dist(1, 15); uniform_int_distribution percent_dist(1, 100); size_t len = len_dist(rng); len *= len * len; size_t percent = percent_dist(rng); uniform_int_distribution pos_dist(0, len - 1); for (size_t j = 0; j < len; ++j) { if (percent_dist(rng) < percent) { fullview.insert(j); } } for (int j = 0; j < 100; ++j) { set_view(navi.get(), fullview); size_t move = move_dist(rng); size_t cur = navi->cur(); if (move == 0) { auto it = fullview.lower_bound(cur); navi->up(); if (fullview.empty()) { CHECK(navi->cur() == G::NO_POS); } else if (it == fullview.begin()) { CHECK(navi->cur() == *it); } else { --it; CHECK(navi->cur() == *it); } } else if (move == 1) { auto it = fullview.upper_bound(cur); navi->down(); if (it == fullview.end()) { if (fullview.empty()) CHECK(navi->cur() == G::NO_POS); else { --it; CHECK(navi->cur() == *it); } } else { CHECK(navi->cur() == *it); } } else if (move == 2) { size_t pos = pos_dist(rng); navi->goto_line(pos, false); CHECK(navi->cur() == pos); } else if (move == 3) { navi->start(); CHECK(navi->cur() == 0); } else if (move == 4) { navi->end(); CHECK(navi->cur() == G::NO_POS); } else if (move == 5) { auto it = fullview.lower_bound(cur); navi->page_up(); for (size_t k = 0; k < 24; ++k) { if (it == fullview.begin()) break; --it; } if (fullview.empty()) { CHECK(navi->cur() == G::NO_POS); } else if (it == fullview.begin()) { CHECK(navi->cur() == *it); } else { --it; CHECK(navi->cur() == *it); } } else if (move == 6) { auto it = fullview.lower_bound(cur); navi->page_down(); for (size_t k = 0; k < 25; ++k) { if (it == fullview.end()) break; ++it; } if (it == fullview.end()) { if (fullview.empty()) CHECK(navi->cur() == G::NO_POS); else { --it; CHECK(navi->cur() == *it); } } else { CHECK(navi->cur() == *it); } } } } } logserver/tests/test_render_queue.cc000066400000000000000000000026261512456611200202720ustar00rootroot00000000000000#include "test.h" #include "render_queue.h" void add_some(RenderQueue* rq, size_t epoch, size_t num) { for (size_t i = 0; i < num; ++i) { stringstream ss; ss << i << "_" << epoch; rq->add(new FormatString(ss.str()), i, epoch); } } void nothing_for(RenderQueue* rq, size_t epoch) { FormatString* fs = nullptr; size_t pos = G::NO_POS; bool ret = rq->remove(epoch, &pos, &fs); CHECK(!ret); CHECK(fs == nullptr); CHECK(pos == G::NO_POS); } void something_for(RenderQueue* rq, size_t epoch, size_t num) { for (size_t i = 0; i < num; ++i) { FormatString* fs = nullptr; size_t pos = G::NO_POS; bool ret = rq->remove(epoch, &pos, &fs); CHECK(ret); string s = *fs; stringstream ss; ss << pos << "_" << epoch; CHECK(s == ss.str()); delete fs; } } TEST_CASE("add and remove") { RenderQueue rq; add_some(&rq, 1, 5); nothing_for(&rq, 0); something_for(&rq, 1, 5); nothing_for(&rq, 1); CHECK(rq.empty()); nothing_for(&rq, 2); } TEST_CASE("stale epoch") { RenderQueue rq; add_some(&rq, 0, 5); add_some(&rq, 1, 5); something_for(&rq, 1, 1); nothing_for(&rq, 0); something_for(&rq, 1, 1); CHECK(!rq.empty()); nothing_for(&rq, 2); nothing_for(&rq, 1); CHECK(rq.empty()); nothing_for(&rq, 2); } TEST_CASE("future epoch") { RenderQueue rq; add_some(&rq, 1, 5); something_for(&rq, 1, 2); add_some(&rq, 2, 5); something_for(&rq, 1, 2); something_for(&rq, 2, 1); nothing_for(&rq, 1); } logserver/tests/test_run.cc000066400000000000000000000037011512456611200164060ustar00rootroot00000000000000/* * Logserver * Copyright (C) 2017-2025 Joel Reardon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test.h" #include "run.h" TEST_CASE("one command") { Run run("echo hello world"); CHECK(run.command() == "echo hello world"); run(); string result = run.read(); CHECK(result == "hello world\n"); } TEST_CASE("one command with data") { string input = "one\ntwo\nthree\nfour\nfive"; string sorted = "five\nfour\none\nthree\ntwo\n"; Run run("sort"); CHECK(static_cast(input.size())== ::write(run.write_fd(), input.data(), input.size())); close(run.write_fd()); CHECK(run.command() == "sort"); run(); string result = run.read(); CHECK(result == sorted); } TEST_CASE("piped command with data") { string input = "one\ntwo\none\none\nfive"; string sorted = "five\none\ntwo\n"; Run run("sort | uniq", input); close(run.write_fd()); run(); run.finish(); string result = run.read(); CHECK(result == sorted); } TEST_CASE("piped command with data and args") { string input = "one\ntwo\none\none\nfive"; stringstream ss; ss << "1 five 3 one 1 two"; Run run("sort | uniq -c", input); close(run.write_fd()); run(); run.finish(); string result = run.read(); stringstream ss_result; ss_result << result; while (ss.good()) { CHECK(ss_result.good()); string s1, s2; ss >> s1; ss_result >> s2; CHECK(s1 == s2); } } logserver/tests/test_tokenizer.cc000066400000000000000000000054021512456611200176140ustar00rootroot00000000000000#include "test.h" #include "tokenizer.h" TEST_CASE("trim") { CHECK(Tokenizer::trim(" abc ") == "abc"); CHECK(Tokenizer::trim("abcd ") == "abcd"); CHECK(Tokenizer::trim("ab cd") == "ab cd"); CHECK(Tokenizer::trim("abc") == "abc"); CHECK(Tokenizer::trim(" bcd") == "bcd"); CHECK(Tokenizer::trim(" \tbcd\t") == "bcd"); CHECK(Tokenizer::trim("a,b,c", "ac") == ",b,"); CHECK(Tokenizer::trim("a b c", "ac") == " b "); CHECK(Tokenizer::trim("a,b,c", ",ac") == "b"); } TEST_CASE("extract") { int i, j; float f; string s, t; bool b; int ret; ret = Tokenizer::extract("%=%", "val=true", &s, &b); CHECK(ret == 2); CHECK(s == "val"); CHECK(b); ret = Tokenizer::extract("%=%", "0=false", &s, &b); CHECK(ret == 2); CHECK(s == "0"); CHECK(!b); ret = Tokenizer::extract("%+2=%", "1+2=3 ", &i, &j); CHECK(ret == 2); CHECK(i == 1); CHECK(j == 3); j = -1; ret = Tokenizer::extract("%-2=%", "1+2=3 ", &i, &j); CHECK(ret == 1); CHECK(i == 1); CHECK(j == -1); ret = Tokenizer::extract("%&%=%&%", "query?a=b&c=0.34&d=e", nullptr, &s, &f, nullptr); CHECK(ret == 4); CHECK(s == "c"); CHECK(f == 0.34f); ret = Tokenizer::extract("function(%, %)", "function(parm1, parm2)", &s, &t); CHECK(ret == 2); CHECK(s == "parm1"); CHECK(t == "parm2"); } TEST_CASE("annotate quote") { string rt = "__________\"'''''''''''\"_________________"; string in = "this is a \"string with\" some quoted part"; string out; Tokenizer::annotate_quote(in, &out); CHECK(out == rt); } TEST_CASE("annotate multi quote") { string rt = "________\"'\"________\"''''\"_\"'''''''''''\""; string in = "this is \"a\" string \"with\" \"some quotes\""; string out; Tokenizer::annotate_quote(in, &out); CHECK(out == rt); } TEST_CASE("split mind quote") { string in = "this,can,\"be,split\",fine"; vector out = {"this", "can", "\"be,split\"", "fine"}; vector ret; Tokenizer::split_mind_quote(in, ",", &ret); CHECK(ret.size() == out.size()); for (size_t i = 0; i < ret.size(); ++i) { CHECK(ret[i] == out[i]); } } TEST_CASE("split mind quote no quotes") { string in = "this,can,be,split,fine"; vector out = {"this", "can", "be", "split", "fine"}; vector ret; Tokenizer::split_mind_quote(in, ",", &ret); CHECK(ret.size() == out.size()); for (size_t i = 0; i < ret.size(); ++i) { CHECK(ret[i] == out[i]); } } TEST_CASE("split mind quote multi") { string in = "this-\"can-be-split\"-\"just-fine\""; vector out = {"this", "\"can-be-split\"", "\"just-fine\""}; vector ret; Tokenizer::split_mind_quote(in, "-", &ret); CHECK(ret.size() == out.size()); for (size_t i = 0; i < ret.size(); ++i) { CHECK(ret[i] == out[i]); } } /* TODO: tests for the rest of tokenizer, however this covers the functions * logserver uses */ logserver/tests/test_typing_line.cc000066400000000000000000000051761512456611200201330ustar00rootroot00000000000000#include #include #include "test.h" #include "typing_line.h" TEST_CASE("insertion") { TypingLine tl; tl.process('h'); tl.process('l'); tl.process('l'); tl.process(KEY_LEFT); tl.process(KEY_LEFT); tl.process('e'); tl.process(KEY_END); tl.process('o'); tl.process('\n'); CHECK(tl.result()); CHECK(tl.typed() == "hello"); } TEST_CASE("insertion home") { TypingLine tl; tl.process('l'); tl.process('l'); tl.process(KEY_HOME); tl.process('e'); tl.process(KEY_LEFT); tl.process('h'); tl.process(KEY_RIGHT); tl.process(KEY_RIGHT); tl.process(KEY_RIGHT); tl.process(KEY_RIGHT); tl.process(KEY_RIGHT); tl.process(KEY_RIGHT); tl.process('o'); tl.process('\n'); CHECK(tl.result()); CHECK(tl.typed() == "hello"); } TEST_CASE("insertion empty") { TypingLine tl; tl.process(KEY_HOME); tl.process(KEY_LEFT); tl.process(KEY_LEFT); tl.process(KEY_LEFT); tl.process('h'); tl.process(KEY_RIGHT); tl.process(KEY_RIGHT); tl.process(KEY_RIGHT); tl.process(KEY_RIGHT); tl.process(KEY_RIGHT); tl.process('i'); tl.process('\n'); CHECK(tl.result()); CHECK(tl.typed() == "hi"); } TEST_CASE("completion") { TypingLine tl; tl.set_type("hello"); tl.process('\n'); tl.set_type("world"); tl.process('\n'); TypingLine other_tl; other_tl.process('h'); other_tl.process('\t'); CHECK(other_tl.typed() == "hello"); other_tl.process(KEY_HOME); other_tl.process('w'); other_tl.process('\t'); CHECK(other_tl.typed() == "world"); } TEST_CASE("completion order") { TypingLine tl; tl.set_type("hello"); tl.process('\n'); tl.set_type("heath"); tl.process('\n'); TypingLine other_tl; other_tl.process('h'); other_tl.process('e'); other_tl.process('\t'); CHECK(other_tl.typed() == "heath"); other_tl.process('\b'); other_tl.process('\b'); other_tl.process('\b'); other_tl.process('\b'); other_tl.process('\b'); other_tl.process('\b'); other_tl.process('\b'); other_tl.process('w'); other_tl.process('w'); other_tl.process('w'); other_tl.process('\t'); CHECK(other_tl.typed() == "www"); } // must be after completion test because it add random words to dictionary TEST_CASE("random typing") { mt19937 rng(random_device{}()); uniform_int_distribution action(0, 1); uniform_int_distribution chardist('a', 'z'); for (int i = 0; i < 100; ++i) { TypingLine tl; CHECK(!tl.has_result()); string s; for (int j = 0; j < 1000; ++j) { if (action(rng)) { char c = chardist(rng); s.push_back(c); tl.process(c); } else { if (!s.empty()) s.pop_back(); tl.process('\b'); } CHECK(!tl.has_result()); CHECK(tl.typed() == s); } tl.process('\n'); CHECK(tl.result()); } }