diff --git a/lci/.gitignore b/lci/.gitignore new file mode 100755 index 0000000..cf572b2 --- /dev/null +++ b/lci/.gitignore @@ -0,0 +1,134 @@ +#prj +users/ +*.key +*.pem +build/ +build_*/ +nn +camera +lci +pmc + +# ide +.vscode/ +.arts/ + +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + + + +# cangjie +build/ +target/ +output/ +module-resolve.json +module-lock.json +cjpm.lock +.cjpm-history +*.macrocall +*.gcda +*.gcno + + + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + + + +# 我加的 +.env/ +*.db +*.api_key +*.key +*.pem +pmcd +rzgs_2 +crt +cert + +# 其他 +log/ +report/ +tmp/ +*.cache +*.pyc +*.cython +#*.jpg +#*.png +*.pth +.eggs/ +*.egg-info +.idea/ +.vscode/ + +# nodejs +node_modules/ + +#python +__pycache__/ +venv + +# do not ignore the log folder by pmc-mgmt +!./mgmt/app/log/ + +# go +go.mod +go.sum + + +# VS +x64 +.vs diff --git a/lci/LICENSE b/lci/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/lci/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 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. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/lci/Makefile b/lci/Makefile new file mode 100644 index 0000000..34bd7fb --- /dev/null +++ b/lci/Makefile @@ -0,0 +1,172 @@ +VERSION := 0.0.4 + +INSTL_DIRS := /usr/local/bin + +CONF_NAME := init.txt + +CONF_DIR := /etc/pmc + +SRV_NAME := pmcd.service + +SRV_DIR := /etc/systemd/system + +CPP:= g++ + +INCS:= -I include/ + +LIBS := -lpthread -lssl -lcrypto -lboost_program_options -lboost_json -lboost_thread + +CPPFLAGS := --std=c++20 -O3 -pipe #-Wall -Werror + +BUILD_DIR = build +$(shell mkdir -p $(BUILD_DIR)) + +#----------- +#$(BUILD_DIR)/libpmc.so: $(BUILD_DIR)/ansi.o $(BUILD_DIR)/utf8.o $(BUILD_DIR)/fsm.o $(BUILD_DIR)/Color.o $(BUILD_DIR)/Matrix.o +# $(CPP) -shared -fPIC $^ $(LIBS) -o $@ + +#pmc: $(BUILD_DIR)/main.o $(BUILD_DIR)/fsm.o $(BUILD_DIR)/Thread.o $(BUILD_DIR)/HttpServer.o +# $(CPP) $^ $(LIBS) -o $@ + +lci: $(BUILD_DIR)/mainLci.o $(BUILD_DIR)/ansi.o $(BUILD_DIR)/utf8.o $(BUILD_DIR)/fsm.o $(BUILD_DIR)/Color.o $(BUILD_DIR)/ku500.o $(BUILD_DIR)/CommonThread.o $(BUILD_DIR)/Drawable.o $(BUILD_DIR)/Area.o $(BUILD_DIR)/FrameBuf.o $(BUILD_DIR)/mem.o $(BUILD_DIR)/I2C.o $(BUILD_DIR)/PCA9685.o $(BUILD_DIR)/MPU6050.o $(BUILD_DIR)/Matrix.o $(BUILD_DIR)/NeuralNetwork.o $(BUILD_DIR)/NNBuilder.o $(BUILD_DIR)/ILayer.o $(BUILD_DIR)/Thread.o + $(CPP) $^ $(LIBS) -ljpeg -lpng -o $@ + +agent: +agent: $(BUILD_DIR)/mainAgent.o $(BUILD_DIR)/WebSocketClient.o + $(CPP) $^ $(LIBS) -lcurl -o $@ + +nn: +nn: $(BUILD_DIR)/mainNn.o $(BUILD_DIR)/Camera.o $(BUILD_DIR)/NeuralNetwork.o $(BUILD_DIR)/Matrix.o $(BUILD_DIR)/ILayer.o $(BUILD_DIR)/NNBuilder.o + $(CPP) $^ $(LIBS) -ljpeg -o $@ + +camera: +camera: $(BUILD_DIR)/mainCamera.o $(BUILD_DIR)/Camera.o + $(CPP) $^ $(LIBS) -ljpeg -o $@ + +$(BUILD_DIR)/mainLci.o: src/mainLci.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/mainMybot.o: src/mainMybot.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/mainAgent.o: src/mainAgent.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/mainNn.o: src/mainNn.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/mainCamera.o: src/mainCamera.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/main.o: src/main.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/ansi.o: src/lci/ansi.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/utf8.o: src/lci/utf8.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/fsm.o: src/lci/fsm.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/Color.o: src/lci/Color.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/ku500.o: src/lci/ku500.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/CommonThread.o: src/lci/CommonThread.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/Drawable.o: src/lci/Drawable.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/Area.o: src/lci/Area.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +#$(BUILD_DIR)/CmdArea.o: src/lci/CmdArea.cpp +# $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +#$(BUILD_DIR)/InputBox.o: src/lci/InputBox.cpp +# $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +#$(BUILD_DIR)/KeyboardEvent.o: src/lci/KeyboardEvent.cpp +# $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/FrameBuf.o: src/lci/FrameBuf.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/mem.o: src/hd/mem.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/I2C.o: src/hd/I2C.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/PCA9685.o: src/hd/PCA9685.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/MPU6050.o: src/hd/MPU6050.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/Camera.o: src/hd/Camera.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/Matrix.o: src/nn/Matrix.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/NeuralNetwork.o: src/nn/NeuralNetwork.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/NNBuilder.o: src/nn/NNBuilder.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/ILayer.o: src/nn/ILayer.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/Thread.o: src/th/Thread.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/LciTask.o: src/LciTask.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/HttpServer.o: src/net/HttpServer.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + +$(BUILD_DIR)/WebSocketClient.o: src/net/WebSocketClient.cpp + $(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@ + + +install: $(APP_NAME) + @echo "正在安装 $(APP_NAME) 到$(INSTL_DIRS)" + @ + #@mkdir -p $(INSTL_DIRS) + @cp -f pmc $(INSTL_DIRS) + #@chmod 755 $(INSTL_DIRS)/pmcd + @ + @echo "正在配置 $(APP_NAME)" + @mkdir -p $(CONF_DIR) + @cp $(CONF_NAME) $(CONF_DIR)/$(CONF_NAME) + @cp $(SRV_NAME) $(SRV_DIR)/$(SRV_NAME) + @systemctl daemon-reload + @systemctl enable $(SRV_NAME) + @systemctl restart $(SRV_NAME) + @ + @echo "安装完成!" + +uninstall: + @echo "正在从 $(INSTL_DIRS) 卸载 $(APP_NAME)" + @ + @rm -f $(INSTL_DIRS)/pmc + #@rm -f $(CONF_DIRS)/$(CONF_NAME) + @ + @systemctl stop $(SRV_NAME) + @systemctl disable $(SRV_NAME) + @rm -f $(SRV_DIRS)/$(SRV_NAME) + @systemctl daemon-reload + @ + @echo "卸载完成!" + +clean: + @rm -rvf $(BUILD_DIR) + @rm -vf lci pmc mybot agent nn diff --git a/lci/README.md b/lci/README.md new file mode 100644 index 0000000..af1688f --- /dev/null +++ b/lci/README.md @@ -0,0 +1,79 @@ +# 进程机器(PMC) +这是一个可以创建、管理子进程以及模块的程序。 + +## pmc是一个什么项目? + +这是一个可扩展的模块,可以在文件夹下建立单独的模块,pmc核心模块可以托管运行这些模块。 + + +## 为什么要做一个这样的项目? + +2024年的时候,我想做一个树莓派机器人,准备用python来写控制程序,可是当时python的多线程能力较弱,所以想到使用子系统,就自己写了一个进程托管程序 + + +## 项目的文件结构是怎样的? + +users用户可以在目录下创建子模块,extra下面还有一些额外的模块。 + +请留意模块的README文件 + + +## 如何托管外部模块 + +编辑启动列表程序,常为init.txt + + +## PMC的安装方式 +``` bash +# 1.安装依赖 +sudo apt update +sudo apt install git g++ make +#sudo apt install python3 python3-pip python3-venv +#sudo dnf install python3-virtualenv +sudo apt install libboost-all-dev +#sudo apt install libssl-dev +#sudo apt install cimg-dev +#sudo apt install libcurl4-openssl-dev +#sudo apt install libcurl4-gnutls-dev +#sudo apt install libargs-dev +#sudo apt install libcpp-httplib-dev + +2. 创建项目 +git clone https://github.com/gopsc/pmc +cd pmc # 这是pmc模块 +make pmc +#sudo make install +``` + +## 加密通信机制(废弃) +PMC在启动时生成一个AES密钥,然后用一个RSA公钥将其加密发送给访问者。 + +然后访问者与客户端之间的交流全部用此AES密钥加密 + + +## 如何使用并发机器运行程序 +首先应该在模块中创建一个启动脚本,脚本中应该完成工作目录的切换、虚拟环境的激活,以及程序的启动。 + +## 需要创建启动脚本(废弃) +```bash +# _run.sh +# 启动脚本 - 示例 +# 需要执行权限 + +#!/bin/bash + +# 切换工作目录 +cd "$(dirname "$0")" || exit + +# 准备工作(安装依赖) +#bash ./_set.sh + +# 准备工作(比如激活虚拟环境) +#source ./.env/bin/activate + +# 执行程序前一定要加exec 否则杀不死 +exec python main.py + +``` + + diff --git a/lci/_run.sh b/lci/_run.sh new file mode 100644 index 0000000..e25f03a --- /dev/null +++ b/lci/_run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd "$(dirname "$0")" || exit +./pmc -i init.txt --serve 5200 diff --git a/lci/_runlci.sh b/lci/_runlci.sh new file mode 100644 index 0000000..6650b28 --- /dev/null +++ b/lci/_runlci.sh @@ -0,0 +1,3 @@ +#!bin/bash +cd "$(dirname "$0")" || exit +exec ./lci --lci --rotate=0 --fontsize=15 diff --git a/lci/extra/_init/.gitignore b/lci/extra/_init/.gitignore new file mode 100644 index 0000000..0a8e29d --- /dev/null +++ b/lci/extra/_init/.gitignore @@ -0,0 +1,110 @@ +# PRJ +.env/ +*.pem +*.key +./_init.csv + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/lci/extra/_init/README.md b/lci/extra/_init/README.md new file mode 100644 index 0000000..d39ecb7 --- /dev/null +++ b/lci/extra/_init/README.md @@ -0,0 +1,5 @@ +# 自启动模块 +这是子系统的自启动模块,该程序是一次性进程,基于 _pmc 库模块。 + +# 做了什么 +启动脚本从 _init.csv 文件中读取模块列表,对每个目标调用 pmc start 子系统调用。 \ No newline at end of file diff --git a/lci/extra/_init/_init.csv b/lci/extra/_init/_init.csv new file mode 100644 index 0000000..b013323 --- /dev/null +++ b/lci/extra/_init/_init.csv @@ -0,0 +1,6 @@ +#extra/chat +#users/smn +#users/dnfs_ex +#users/fortress +users/sensors + diff --git a/lci/extra/_init/_run.sh b/lci/extra/_init/_run.sh new file mode 100644 index 0000000..b7467a5 --- /dev/null +++ b/lci/extra/_init/_run.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd "$(dirname "$0")" || exit +bash ./_setup.sh +source ./.env/bin/activate +exec python init.py diff --git a/lci/extra/_init/_setup.sh b/lci/extra/_init/_setup.sh new file mode 100644 index 0000000..5c9e392 --- /dev/null +++ b/lci/extra/_init/_setup.sh @@ -0,0 +1,7 @@ +#!/bin/bash +cd "$(dirname "$0")" || exit +python3 -m venv .env +source ./.env/bin/activate +source ./_src.sh +#python -m pip install --upgrade pip +pip install -r requirements.txt diff --git a/lci/extra/_init/_src.sh b/lci/extra/_init/_src.sh new file mode 100644 index 0000000..0c4d9bc --- /dev/null +++ b/lci/extra/_init/_src.sh @@ -0,0 +1,2 @@ +#pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple +pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple/ diff --git a/lci/extra/_init/init.py b/lci/extra/_init/init.py new file mode 100644 index 0000000..becd437 --- /dev/null +++ b/lci/extra/_init/init.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +''' + +子系统启动脚本 +这个脚本的功能是通过PMC调用完成子系统的初始化 + +''' +from pmc import PmcComm +import os +import csv +import time +script_dir = os.path.dirname(os.path.abspath(__file__)) +with open(f"{script_dir}/_init.csv", encoding='utf-8') as f: + reader = csv.reader(f) + records = [row[0] for row in reader] +def main(): + comm = PmcComm('http://localhost:8012') + comm.Set_RSA_Pri_Key('../pmc/private_key.pem') + comm.Try_Get_AES_Key() + for item in records: + if item and not item.startswith('#'): + comm.try_start(item) + time.sleep(1) # 保持一定的时间间隔,避免CPU占用 +if __name__ == "__main__": + main() diff --git a/lci/extra/_init/requirements.txt b/lci/extra/_init/requirements.txt new file mode 100644 index 0000000..93e3a80 --- /dev/null +++ b/lci/extra/_init/requirements.txt @@ -0,0 +1,5 @@ +flask +waitress +GitPython>=3.1.0 +setuptools>=40.8.0 +../_pmc diff --git a/lci/extra/_pmc/.gitignore b/lci/extra/_pmc/.gitignore new file mode 100644 index 0000000..58b81c2 --- /dev/null +++ b/lci/extra/_pmc/.gitignore @@ -0,0 +1,109 @@ +# PRJ +.env/ +*.pem +*.key + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/lci/extra/_pmc/README.md b/lci/extra/_pmc/README.md new file mode 100644 index 0000000..2ac3f66 --- /dev/null +++ b/lci/extra/_pmc/README.md @@ -0,0 +1,8 @@ +## 类模块 + +这是一个类模块,它基于pmc,部分调用基于dnfs、cops、chat_s。 + + +## 包含什么 + +该类模块中包含对于python需要的子系统调用,其中聊天室的通信函数主要用于日志的输出等行为。 \ No newline at end of file diff --git a/lci/extra/_pmc/setup.py b/lci/extra/_pmc/setup.py new file mode 100644 index 0000000..7dcf2db --- /dev/null +++ b/lci/extra/_pmc/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup, find_packages + +setup( + name="pmc", + version="0.0.9", + packages=find_packages(where="src"), + package_dir={'': 'src'}, + install_requires=[ + 'setuptools', + 'requests', + 'cryptography', + ], + setup_requires=[ + #"whell", + ], + python_requires='>=3.8' +) diff --git a/lci/extra/cancerai/.gitignore b/lci/extra/cancerai/.gitignore new file mode 100644 index 0000000..e62b9b4 --- /dev/null +++ b/lci/extra/cancerai/.gitignore @@ -0,0 +1 @@ +api-test/ diff --git a/lci/extra/cancerai/README.md b/lci/extra/cancerai/README.md new file mode 100644 index 0000000..158d435 --- /dev/null +++ b/lci/extra/cancerai/README.md @@ -0,0 +1,5 @@ +# 作者是CANCERAI +这个模块的作者写了mgmt模块的前端部分,这个模块曾经短暂地为mgmt模块提供一部分的后端支持。 + +# 20250810 +该模块已经被开发成为dnfs树状网络文件系统的后端。 \ No newline at end of file diff --git a/lci/extra/cancerai/_run.sh b/lci/extra/cancerai/_run.sh new file mode 100644 index 0000000..6a7ae61 --- /dev/null +++ b/lci/extra/cancerai/_run.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd "$(dirname "$0")" || exit +cd pmc-backend +exec mvn clean spring-boot:run diff --git a/lci/extra/cancerai/api-doc.html b/lci/extra/cancerai/api-doc.html new file mode 100644 index 0000000..458094f --- /dev/null +++ b/lci/extra/cancerai/api-doc.html @@ -0,0 +1,1129 @@ + + + + + + + Document + + + + +

API文档

+
+
目录
+ 登陆 +
+ 获取节点 +
+ 新增节点 +
+ 删除节点 +
+ 修改节点 +
+ +
登陆
+

URL

+

127.0.0.1:8011/api/login

+ +

请求方法

+

POST

+ +

请求头

+

无

+ +

请求体

+

类型: JSON

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namepassword
说明用户名密码
是否必须TrueTrue
类型StringString
示例cancerai123
+

响应体

+

状态码: 200

+

类型: JSON

+

说明: 登陆成功

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息token
类型StringString
示例"登陆成功""eyJhbGciOiJIUzI1NiJ9....."
+
+

状态码: 401

+

类型: JSON

+

说明: 登陆失败

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"账号或密码错误"null
+
+

状态码: 405

+

类型: JSON

+

说明: 请求方式不支持

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"请求方式错误"null
+

+
获取节点
+

URL

+

127.0.0.1:8011/api/get-notes

+

请求方法

+

GET

+

请求头

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
token
说明登陆时拿到的令牌
是否必须True
类型String
示例"eyJhbGciOiJIUzI1NiJ9....."
+

查询参数

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
token
说明PID
是否必须True
类型int
示例0 +
+

请求体

+

无

+

响应体

+

状态码: 200

+

类型: JSON

+

说明: 查询成功

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息查询到的Note
类型StringList
示例"Request OK"[{}, {}, {}]
+
+

状态码: 401

+

类型: JSON

+

说明: JWT令牌无效

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"JWT无效"null
+
+

状态码: 401

+

类型: JSON

+

说明: JWT令牌过期

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"JWT过期"null
+
+

状态码: 405

+

类型: JSON

+

说明: 请求方式不支持

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"请求方式错误"null
+
+

状态码: 500

+

类型: JSON

+

说明: 没有传PID或其他问题

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"服务器内部错误"null
+

+
新增节点
+

URL

+

127.0.0.1:8011/api/add-note

+ +

请求方法

+

PUT

+ +

请求头

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
token
说明登陆时拿到的令牌
是否必须True
类型String
示例"eyJhbGciOiJIUzI1NiJ9....."
+ +

请求体

+

无

+ +

查询参数

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
pidtitlecontent
说明PIDnote标题note主体
是否必须TrueTrueTrue
类型intStringString
示例0"test title""test content"
+

响应体

+

状态码: 200

+

类型: JSON

+

说明: 新增成功

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"成功添加节点"null +
+
+

状态码: 401

+

类型: JSON

+

说明: JWT无效或被篡改

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"JWT无效"null
+
+

状态码: 401

+

类型: JSON

+

说明: JWT令牌过期

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"JWT过期"null
+
+

状态码: 405

+

类型: JSON

+

说明: 请求方式不支持

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"请求方式错误"null
+
+

状态码: 500

+

类型: JSON

+

说明: 可能请求参数不正确或服务器内部错误

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"服务器内部错误"null
+

+
删除节点
+

URL

+

127.0.0.1:8011/api/delete-note

+ +

请求方法

+

DELETE

+ +

请求头

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
token
说明登陆时拿到的令牌
是否必须True
类型String
示例"eyJhbGciOiJIUzI1NiJ9....."
+ +

请求体

+

无

+ +

查询参数

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
id
说明PID
是否必须True
类型int
示例16
+

响应体

+

状态码: 200

+

类型: JSON

+

说明: 删除成功

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"成功删除节点"null +
+
+

状态码: 401

+

类型: JSON

+

说明: JWT无效或被篡改

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"JWT无效"null
+
+

状态码: 401

+

类型: JSON

+

说明: JWT令牌过期

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"JWT过期"null
+
+

状态码: 405

+

类型: JSON

+

说明: 请求方式不支持

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"请求方式错误"null
+
+

状态码: 500

+

类型: JSON

+

说明: 删除不存在的id或服务器内部错误

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"服务器内部错误"null
+

+
更新节点
+

URL

+

127.0.0.1:8011/api/update-note

+ +

请求方法

+

PATCH

+ +

请求头

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
token
说明登陆时拿到的令牌
是否必须True
类型String
示例"eyJhbGciOiJIUzI1NiJ9....."
+ +

请求体

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idpidtitlecontent
说明要修改的note ID修改后的note的PID修改后的note标题修改后的note主体
是否必须TrueTrueTrueTrue
类型intintStringString
示例160"changed test""changed content"
+ +

查询参数

+

无

+

响应体

+

状态码: 200

+

类型: JSON

+

说明: 更新成功

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"成功更新节点"null +
+
+

状态码: 401

+

类型: JSON

+

说明: JWT无效或被篡改

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"JWT无效"null
+
+

状态码: 401

+

类型: JSON

+

说明: JWT令牌过期

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"JWT过期"null
+
+

状态码: 405

+

类型: JSON

+

说明: 请求方式不支持

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"请求方式错误"null
+
+

状态码: 500

+

类型: JSON

+

说明: 更新的id不存在或服务器内部错误

+

字段:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
messagedata
说明说明信息无
类型Stringnull
示例"服务器内部错误"null
+ + + \ No newline at end of file diff --git a/lci/extra/cancerai/pmc-backend/.gitattributes b/lci/extra/cancerai/pmc-backend/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/lci/extra/cancerai/pmc-backend/.gitignore b/lci/extra/cancerai/pmc-backend/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/lci/extra/cancerai/pmc-backend/pom.xml b/lci/extra/cancerai/pmc-backend/pom.xml new file mode 100644 index 0000000..120ee4a --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.3 + + + + org.cancerai + pmc-backend + 0.0.1-SNAPSHOT + pmc-backend + pmc management backend + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-webflux + + + com.google.code.gson + gson + 2.13.1 + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/PmcBackendApplication.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/PmcBackendApplication.java new file mode 100644 index 0000000..b7d8287 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/PmcBackendApplication.java @@ -0,0 +1,15 @@ +package org.cancerai.pmc_backend; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletComponentScan; + +@ServletComponentScan +@SpringBootApplication +public class PmcBackendApplication { + + public static void main(String[] args) { + SpringApplication.run(PmcBackendApplication.class, args); + } + +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/controller/DNFSController.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/controller/DNFSController.java new file mode 100644 index 0000000..9a413fe --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/controller/DNFSController.java @@ -0,0 +1,74 @@ +package org.cancerai.pmc_backend.controller; + +import jakarta.servlet.http.HttpServletResponse; +import org.cancerai.pmc_backend.entility.Note; +import org.cancerai.pmc_backend.utils.DNFSUtils; +import org.cancerai.pmc_backend.utils.HttpResponseText; +import org.cancerai.pmc_backend.vo.Result; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; + +@CrossOrigin(origins = "*") +@RestController +@RequestMapping("/api") +public class DNFSController { + @GetMapping("/get-notes") + public static Result getNotes(@RequestParam int pid) { + return Result.success(HttpResponseText.SUCCESS_TO_REQUEST, DNFSUtils.getNodes(pid)); + } + + @PutMapping("/add-note") + public Result addNote( + @RequestParam int pid, + @RequestParam String title, + @RequestParam String content, + HttpServletResponse response + ) { + HashMap re = DNFSUtils.addNote(pid, title, content); + String value = re.get("message"); + + if ("Note added successfully".equals(value)) { + return Result.success(HttpResponseText.SUCCESS_TO_ADD_NOTE); + } else { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return Result.error(HttpResponseText.INTERNAL_SERVER_ERROR); + } + } + + @DeleteMapping("/delete-note") + public Result deleteNote( + @RequestParam int id, + HttpServletResponse response + ) { + HashMap re = DNFSUtils.deleteNote(id); + String value = re.get("message"); + + if ("Note deleted successfully".equals(value)) { + return Result.success(HttpResponseText.SUCCESS_TO_DELETE_NOTE); + } else { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return Result.error(HttpResponseText.INTERNAL_SERVER_ERROR); + } + } + + @PatchMapping("/update-note") + public static Result updateNote( + @RequestBody Note note, + HttpServletResponse response + ) { + int id = note.getId(); + int pid = note.getPid(); + String title = note.getTitle(); + String content = note.getContent(); + + HashMap re = DNFSUtils.updateNote(id, pid, title, content); + String value = re.get("message"); + if ("Your change is up-to-date.".equals(value)) { + return Result.success(HttpResponseText.SUCCESS_TO_UPDATE_NOTE); + } else { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return Result.error(HttpResponseText.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/controller/LoginController.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/controller/LoginController.java new file mode 100644 index 0000000..8baf1cb --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/controller/LoginController.java @@ -0,0 +1,29 @@ +package org.cancerai.pmc_backend.controller; + +import jakarta.servlet.http.HttpServletResponse; +import org.cancerai.pmc_backend.entility.User; +import org.cancerai.pmc_backend.service.LoginService; +import org.cancerai.pmc_backend.utils.EncryptUtils; +import org.cancerai.pmc_backend.vo.Result; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@CrossOrigin(origins = "*") +@RestController +@RequestMapping("/api") +public class LoginController { + @Autowired + private LoginService loginService; + + @PostMapping("/login") + public Result login( + @RequestBody User user, + HttpServletResponse response + ) { + String name = user.getName(); + String password = EncryptUtils.SHA256Encrypt(user.getPassword()); + + return loginService.verifyUser(name, password, response); + } + +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/controller/ProcessController.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/controller/ProcessController.java new file mode 100644 index 0000000..2bbf090 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/controller/ProcessController.java @@ -0,0 +1,52 @@ +package org.cancerai.pmc_backend.controller; + +import org.cancerai.pmc_backend.entility.ProcessEntility; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.ArrayList; + +@CrossOrigin(origins = "*") +@RestController() +public class ProcessController { + @GetMapping("/get-process") + public ArrayList cleanProcess() { + ArrayList arr = getProcessList(); + ArrayList result = new ArrayList<>(); + final String DEAD = "Dead"; + +// exclude Dead lines + for (String i : arr) { + String[] item = i.split("\t"); + String pid = item[0]; + String status = item[1]; + String cmd = item[2]; + + if (DEAD.equals(status)) { + continue; + } + result.add(new ProcessEntility(pid, status, cmd)); + } + + return result; + } + + private static ArrayList getProcessList() { + ArrayList array = new ArrayList<>(); + WebClient webClient = WebClient.create("http://127.0.0.1:8012"); + + String response = webClient.post() + .uri("/api/list") + .retrieve() + .bodyToMono(String.class) + .block(); + + String[] splitRes = response.split("\n"); + for (String item : splitRes) { + array.add(item); + } + return array; + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/entility/Note.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/entility/Note.java new file mode 100644 index 0000000..1769541 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/entility/Note.java @@ -0,0 +1,57 @@ +package org.cancerai.pmc_backend.entility; + +public class Note { + private String content; + private int id; + private int pid; + private String title; + + public Note(String content, int id, int pid, String title) { + this.content = content; + this.id = id; + this.pid = pid; + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getPid() { + return pid; + } + + public void setPid(int pid) { + this.pid = pid; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String toString() { + return "Note{" + + "content='" + content + '\'' + + ", id=" + id + + ", pid=" + pid + + ", title='" + title + '\'' + + '}'; + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/entility/ProcessEntility.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/entility/ProcessEntility.java new file mode 100644 index 0000000..41402ef --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/entility/ProcessEntility.java @@ -0,0 +1,37 @@ +package org.cancerai.pmc_backend.entility; + +public class ProcessEntility { + private String pid; + private String status; + private String cmd; + + public ProcessEntility(String pid, String status, String cmd) { + this.pid = pid; + this.status = status; + this.cmd = cmd; + } + + public String getPid() { + return pid; + } + + public void setPid(String pid) { + this.pid = pid; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/entility/User.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/entility/User.java new file mode 100644 index 0000000..1db1b40 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/entility/User.java @@ -0,0 +1,22 @@ +package org.cancerai.pmc_backend.entility; + +public class User { + private String name; + private String password; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/exception/GlobalExceptionHandler.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..188bc53 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/exception/GlobalExceptionHandler.java @@ -0,0 +1,45 @@ +package org.cancerai.pmc_backend.exception; + +import org.cancerai.pmc_backend.vo.Result; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +@RestControllerAdvice +public class GlobalExceptionHandler { +// 400 + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(HttpMessageNotReadableException.class) + public static Result JWTSignature(HttpMessageNotReadableException e) { + e.printStackTrace(); + return Result.error("请求参数无效"); + } + + // 401 + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public static Result requestMethodNotSupport(HttpRequestMethodNotSupportedException e) { + e.printStackTrace(); + return Result.error("请求方法错误"); + } + + // 404 + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(NoResourceFoundException.class) + public static Result ResourceNotFound(NoResourceFoundException e) { + e.printStackTrace(); + return Result.error("未找到静态资源"); + } + +// 500 + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public static Result exception(Exception e) { + e.printStackTrace(); + return Result.error("服务器内部错误"); + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/filter/LoginFilter.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/filter/LoginFilter.java new file mode 100644 index 0000000..5dd5c75 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/filter/LoginFilter.java @@ -0,0 +1,91 @@ +package org.cancerai.pmc_backend.filter; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import jakarta.servlet.*; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.cancerai.pmc_backend.utils.HttpResponseText; +import org.cancerai.pmc_backend.utils.JWTUtils; +import org.cancerai.pmc_backend.vo.Result; +import org.springframework.core.annotation.Order; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebFilter(urlPatterns = "/api/*") +@Order(1) +public class LoginFilter implements Filter { + final String[] EXCLUDE_URL_LIST = { + "/api/login" + }; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; +// set content security policy + response.setHeader("Content-Security-Policy", "default-src *"); + +// exclude url + String uri = request.getRequestURI(); + for (String item : EXCLUDE_URL_LIST) { + if (item.equals(uri)) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + } +// Allow OPTION method + if ("OPTIONS".equals(request.getMethod())) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + String token = request.getHeader("token"); + if (token == null || token.isEmpty()) { +// set response header + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + +// construct body + Gson gson = new GsonBuilder().serializeNulls().create(); + String responseBody = gson.toJson(Result.error(HttpResponseText.UNAUTHORIZED)).toString(); +// set response body + PrintWriter writer = response.getWriter(); + writer.write(responseBody); + writer.flush(); + return; + } + + try { + JWTUtils.parseJWT(token); + } catch (MalformedJwtException e) { +// set response body + Gson gson = new GsonBuilder().serializeNulls().create(); + String body = gson.toJson(Result.error(HttpResponseText.JWT_MALFORMED)); + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + PrintWriter writer = response.getWriter(); + writer.write(body); + writer.flush(); + return; + } catch (ExpiredJwtException e) { +// set response body + Gson gson = new GsonBuilder().serializeNulls().create(); + String body = gson.toJson(Result.error(HttpResponseText.JWT_EXPIRED)); + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + PrintWriter writer = response.getWriter(); + writer.write(body); + writer.flush(); + return; + } + +// Allow + filterChain.doFilter(servletRequest, servletResponse); + response.setHeader("Content-Security-Policy", "default-src *"); + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/service/LoginService.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/service/LoginService.java new file mode 100644 index 0000000..5d1f33e --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/service/LoginService.java @@ -0,0 +1,67 @@ +package org.cancerai.pmc_backend.service; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import jakarta.servlet.http.HttpServletResponse; +import org.cancerai.pmc_backend.entility.Note; +import org.cancerai.pmc_backend.utils.DNFSUtils; +import org.cancerai.pmc_backend.utils.JWTUtils; +import org.cancerai.pmc_backend.vo.Result; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +@Service +public class LoginService { + public static Result verifyUser(String name, String password, HttpServletResponse response) { + ArrayList> allUsersInfo = getAllUsersInfo(); + boolean isExist = false; + + for (HashMap u : allUsersInfo) { + if (name.equals(u.get("name")) + && password.equals(u.get("password")) + ) { + isExist = true; + } + } + + if (isExist) { + return Result.success( + "登陆成功", + JWTUtils.genJWT(name, password)); + } else { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return Result.error("账号或密码错误"); + } + } + + private static ArrayList> getAllUsersInfo() { + Gson gson = new Gson(); +// StringBuilder stringBuilder = new StringBuilder(); + ArrayList userList = new ArrayList<>(); + ArrayList re = DNFSUtils.getNodes(0); + String jsonString = gson.toJson(re); + JsonArray jsonArray = gson.fromJson(jsonString, JsonArray.class); + + for (JsonElement jsonElement : jsonArray) { +// To json + JsonObject jsonObject = jsonElement.getAsJsonObject(); +// The value is \" + VALUE + \" + String titleString = jsonObject.get("title").toString(); + String titleValue = titleString.substring(1, titleString.length() - 1); + if ("Users".equals(titleValue)) { + String contentString = jsonObject.get("content").toString(); + String contentValue = contentString.substring(1, contentString.length() - 1); + + String[] contentSplit = contentValue.split("\\\\n"); + userList.addAll(Arrays.asList(contentSplit)); + } + } + + return DNFSUtils.parseUserInfo(userList); + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/DNFSUtils.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/DNFSUtils.java new file mode 100644 index 0000000..9cf36e0 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/DNFSUtils.java @@ -0,0 +1,105 @@ +package org.cancerai.pmc_backend.utils; + +import org.cancerai.pmc_backend.entility.Note; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; + +public class DNFSUtils { + private final static String domain = "http://qsont.xyz:8006"; + + public static ArrayList getNodes(Integer pid) { + RestTemplate restTemplate = new RestTemplate(); +// prepare request body + HashMap requestBody = new HashMap<>(); + requestBody.put("pid", pid); +// prepare request header + HttpHeaders requestHeader = new HttpHeaders(); + requestHeader.setContentType(MediaType.APPLICATION_JSON); + + ResponseEntity> response = restTemplate.exchange( + domain + "/get_notes", + HttpMethod.POST, + new HttpEntity<>(requestBody, requestHeader), + new ParameterizedTypeReference>() { + } + ); + + return response.getBody(); + } + + public static HashMap addNote(int pid, String title, String content) { + RestTemplate restTemplate = new RestTemplate(); + HashMap body = new HashMap<>(); + HttpHeaders header = new HttpHeaders(); + + header.setContentType(MediaType.APPLICATION_JSON); + body.put("pid", String.valueOf(pid)); + body.put("title", title); + body.put("content", content); + + return restTemplate.exchange( + domain + "/add_note", + HttpMethod.POST, + new HttpEntity<>(body, header), + new ParameterizedTypeReference>() { + } + ).getBody(); + } + + public static HashMap deleteNote(int id) { + RestTemplate restTemplate = new RestTemplate(); + HashMap body = new HashMap<>(); + HttpHeaders header = new HttpHeaders(); + + header.setContentType(MediaType.APPLICATION_JSON); + body.put("id", String.valueOf(id)); + + return restTemplate.exchange( + domain + "/delete_note", + HttpMethod.POST, + new HttpEntity<>(body, header), + new ParameterizedTypeReference>() { + } + ).getBody(); + } + + public static HashMap updateNote(int id, int pid, String title, String content) { + RestTemplate restTemplate = new RestTemplate(); + HashMap body = new HashMap<>(); + HttpHeaders header = new HttpHeaders(); + + header.setContentType(MediaType.APPLICATION_JSON); + body.put("id", String.valueOf(id)); + body.put("pid", String.valueOf(pid)); + body.put("title", title); + body.put("content", content); + + return restTemplate.exchange( + domain + "/update_note", + HttpMethod.POST, + new HttpEntity<>(body, header), + new ParameterizedTypeReference>() { + } + ).getBody(); + } + + public static ArrayList> parseUserInfo(ArrayList userLIst) { + ArrayList> array = new ArrayList<>(); + + for (String s : userLIst) { + HashMap map = new HashMap<>(); + String[] splitArray = s.split("::"); + map.put("id", splitArray[0]); + map.put("name", splitArray[1]); + map.put("password", splitArray[2]); + map.put("role", splitArray[3]); + array.add(map); + } + + return array; + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/EncryptUtils.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/EncryptUtils.java new file mode 100644 index 0000000..c0d941f --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/EncryptUtils.java @@ -0,0 +1,30 @@ +package org.cancerai.pmc_backend.utils; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class EncryptUtils { + public static String SHA256Encrypt(String str) { + try { + // 获取SHA-256算法实例 + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + + // 计算哈希值(指定UTF-8编码避免平台差异) + byte[] hashBytes = digest.digest(str.getBytes(StandardCharsets.UTF_8)); + + // 将字节数组转换为十六进制字符串 + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + // 将每个字节转换为两位十六进制数,不足两位补0 + String hex = String.format("%02x", 0xFF & b); + hexString.append(hex); + } + return hexString.toString(); + + } catch (NoSuchAlgorithmException e) { + // SHA-256是Java标准算法,理论上不会抛出此异常 + throw new RuntimeException("SHA-256算法不支持", e); + } + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/HttpResponseText.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/HttpResponseText.java new file mode 100644 index 0000000..f6100be --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/HttpResponseText.java @@ -0,0 +1,14 @@ +package org.cancerai.pmc_backend.utils; + +public class HttpResponseText { +// success + public final static String SUCCESS_TO_REQUEST = "Request OK"; + public final static String SUCCESS_TO_ADD_NOTE = "成功添加节点"; + public final static String SUCCESS_TO_DELETE_NOTE = "成功删除节点"; + public final static String SUCCESS_TO_UPDATE_NOTE = "成功更新节点"; +// error + public final static String UNAUTHORIZED = "Cannot authorized identity, access denied"; + public final static String JWT_MALFORMED = "JWT无效"; + public final static String JWT_EXPIRED = "JWT过期"; + public final static String INTERNAL_SERVER_ERROR = "服务器内部错误"; +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/JWTUtils.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/JWTUtils.java new file mode 100644 index 0000000..ca8758a --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/utils/JWTUtils.java @@ -0,0 +1,42 @@ +package org.cancerai.pmc_backend.utils; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Base64; +import java.util.Date; + +public class JWTUtils { + final private static String key = "cancerai_key_qpalzm102938"; + final private static String encodedKey = Base64.getEncoder() + .encodeToString(key.getBytes(StandardCharsets.UTF_8)); + // set JWT kwy + final static Key jwtKey = Keys.hmacShaKeyFor(encodedKey.getBytes()); + + public static String genJWT(String name, String password) { + final Date expirationTime = new Date( + System.currentTimeMillis() + + (60 * 1000 // 1min + * 60 // 1h + * 24 // 1day + * 2 // 2 days + )); + + return Jwts.builder() + .claim("userName", name) + .claim("user-password", password) + .signWith(jwtKey, SignatureAlgorithm.HS256) + .setExpiration(expirationTime) + .compact(); + } + + public static void parseJWT(String jwt) { + Jwts.parserBuilder() + .setSigningKey(jwtKey) + .build() + .parseClaimsJws(jwt); + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/vo/Result.java b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/vo/Result.java new file mode 100644 index 0000000..7d6aac6 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/java/org/cancerai/pmc_backend/vo/Result.java @@ -0,0 +1,44 @@ +package org.cancerai.pmc_backend.vo; + +public class Result { + private String message; + private Object data; + + private Result(String message, Object data) { + this.message = message; + this.data = data; + } + + public static Result success(String message) { + return new Result(message, null); + } + + public static Result success(String message, Object data) { + return new Result(message, data); + } + + public static Result error(String message) { + return new Result(message, null); + } + + public static Result error(String message, Object data) { + return new Result(message, data); + } + + // getter and setter + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } +} diff --git a/lci/extra/cancerai/pmc-backend/src/main/resources/application.yaml b/lci/extra/cancerai/pmc-backend/src/main/resources/application.yaml new file mode 100644 index 0000000..c76c8f2 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/main/resources/application.yaml @@ -0,0 +1,2 @@ +server: + port: 8011 diff --git a/lci/extra/cancerai/pmc-backend/src/test/java/org/cancerai/pmc_backend/PmcBackendApplicationTests.java b/lci/extra/cancerai/pmc-backend/src/test/java/org/cancerai/pmc_backend/PmcBackendApplicationTests.java new file mode 100644 index 0000000..21bca04 --- /dev/null +++ b/lci/extra/cancerai/pmc-backend/src/test/java/org/cancerai/pmc_backend/PmcBackendApplicationTests.java @@ -0,0 +1,21 @@ +package org.cancerai.pmc_backend; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class PmcBackendApplicationTests { + + @Test + void contextLoads() { + StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append('a'); + System.out.println(stringBuilder); + + stringBuilder.setLength(0); + stringBuilder.append('b'); + System.out.println(stringBuilder); + } + +} diff --git a/lci/extra/chat/.gitignore b/lci/extra/chat/.gitignore new file mode 100644 index 0000000..887780d --- /dev/null +++ b/lci/extra/chat/.gitignore @@ -0,0 +1,107 @@ +# PRJ +.env/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/lci/extra/chat/README.md b/lci/extra/chat/README.md new file mode 100644 index 0000000..a898faa --- /dev/null +++ b/lci/extra/chat/README.md @@ -0,0 +1,33 @@ +## chat是一个什么项目 +该模块是python TCP socket聊天室服务,对每个连接者提供消息转发的服务。 + +你还可以召唤AI,与它进行对话。 + +## 如何运行chat服务? + +我们在项目中打包了启动脚本```_run.sh```,你只需要安装好依赖的软件,就可以一键创建虚拟环境并且执行它。 + +```shell +sudo apt update +sudo apt install python3 python3-pip python3-venv +bash _run.sh +``` + +## 如何连接chat服务? +```shell +python client.py [--addr=localhost] [--port=8003] [--name=Unknow] +``` + +## 我如何与AI对话 + +服务器从文件中读取提示词,并且将具有``````特定前缀的消息转发给AI。 + +并且具有一定的记忆能力。 + +## 什么是快捷标签输入 + +成功连接后输入```chatgpt```将会给接下来的对话添加``````标签。 + +在聊天框中输入```where```查看当前的对话标签。 + +在聊天框中输入```del```以删除当前的前缀。 \ No newline at end of file diff --git a/lci/extra/chat/_run.sh b/lci/extra/chat/_run.sh new file mode 100644 index 0000000..f8a26e5 --- /dev/null +++ b/lci/extra/chat/_run.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd "$(dirname "$0")" || exit +bash ./_set.sh 1>/dev/null +source ./.env/bin/activate +exec python server.py --type=server --port=8003 --addr=0.0.0.0 diff --git a/lci/extra/chat/_set.sh b/lci/extra/chat/_set.sh new file mode 100644 index 0000000..a7f97e5 --- /dev/null +++ b/lci/extra/chat/_set.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd "$(dirname "$0")" || exit +python3 -m venv .env +source .env/bin/activate +pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ diff --git a/lci/extra/chat/client.py b/lci/extra/chat/client.py new file mode 100644 index 0000000..2ad7b5b --- /dev/null +++ b/lci/extra/chat/client.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +#------------------------------------------------------------- +# 以下区域是我的代码 +''' + +【项目简介】 网络聊天室 v0.0.2 + +【加入项目】 +注意代码只添加不修改,你可以写个函数,署名 +不要修改别人的代码。 + +【基线规则】 +在自己的区域修改代码,谁的代码能跑就加入主线 +除此之外只在各自的测试区域内运行 + +【设计思路】 +建议一个项目一个文件先,容易分发 + +【测试方式】 +服务器进程: python chat.py --type=server +客户端进程: python chat.py + +-------------------------------- +-------------------------------- +-------------------------------- +【项目名称】 网络聊天室 v 0.0.3 (20250524) +我们会将新版本加入pmc.cops中作为常驻服务 + +【改进计划】 +1.我们专门设置了一个只发送一条消息就关闭的函数以供别的文件调用 +2.我们将端口暴露在了公网下,以测试连接情况,发现并修复了许多bug。 +3.我们设置欢迎信息只在服务端可见。 + +【使用方法】 +python chat.py [--type=server|client] [--addr=localhost] [--port=8003] [--name=Unknow] + +【记得反馈!】 +期待您将反馈发送至gitee + +---------------------------------------- +######################################## +######################################## + +网络聊天室 version 0.1.0 + +使用方法 python chat.py [--addr=localhost] [--port=8003] [--name=Unknow] +''' +import argparse +import socket +import select +import threading +import sys +import time + + +class ChatClient: + + def __init__(self, addr: str, port: int, your_name: str): + self.addr: str = addr + self.port: int = port + self.name: str = your_name + + def send_client(self, msg: str): + '''发送消息给聊天室服务器。''' + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(5) # 设置连接超时 + s.connect((self.addr, self.port)) + s.settimeout(None) # 连接成功后取消超时 + s.sendall(f"{self.name}: {msg}".encode('utf-8')) + except (ConnectionRefusedError, socket.timeout): + print(f"Could not connect to {self.addr}:{self.port} - message not sent") + except Exception as e: + print(f"Unexpected error while sending message: {e}") + + def recv_loop(self, s: socket.socket): + """ + 接收消息的函数,持续从服务器接收消息并打印。 + Args: + s (socket.socket): 连接的套接字对象 + + 使用select监视套接字状态,当套接字可读时接收消息并打印。 + """ + socklist = [s] + while self.running: + + try: + readable, _, exception = select.select(socklist, [], socklist, 0.1) + + if readable != []: + try: + msg = readable[0].recv(8092) + if msg: + print(msg.decode('utf-8')) + else: + # 服务器关闭连接 + print("Connection closed by server.") + return False # 返回False表示需要重连 + except ConnectionResetError: + print("Connection reset by server.") + return False # 返回False表示需要重连 + + if exception != []: + print("Exception on socket, closing...") + return False # 返回False表示需要重连 + + except ValueError: + ... # 程序退出了会抛出这个异常 + except OSError as e: + # 处理socket错误 + print(f"Socket error: {e}") + return False # 返回False表示需要重连 + + return True # 正常退出 + + + def _lexer(self, input_data: str): + '''如果不需要发送则返回None。''' + if input_data == "quit": + self.running = False + return None + elif not self.extra_cmd and input_data == "chatgpt": + self.extra_cmd = "" + return None + elif self.extra_cmd and input_data == "del": + self.extra_cmd = None + return None + elif input_data == "where": + print(f'---{self.extra_cmd}---') + return None + else: + return input_data + + def _mixup_msg(self, input_data): + if self.extra_cmd: + return f"{self.extra_cmd}{input_data}" + else: + return input_data + + def send_p(self,): + '''读取并且发送消息的进程''' + self.extra_cmd = None + while self.running: + try: + # 使用input()替代select.select(),更兼容Windows + input_data = input().strip() + input_data = self._lexer(input_data) + if input_data: # 如果不需要发送,_lexer返回None + input_data = self._mixup_msg(input_data) + self.send_client(input_data) + except EOFError: + # 处理Ctrl+D等情况 + break + except Exception as e: + # 捕获其他异常,防止程序崩溃 + print(f"Input error: {e}") + continue + + def _connect_and_print(self, s: socket.socket): + s.connect((self.addr, self.port)) + print(f'Connected to {self.addr}:{self.port}') + + def _sleep_if_reconnect(self, reconnect_count: int): + """根据重连次数计算等待时间,实现指数退避策略""" + if self.running: + # 指数退避:1, 2, 4, 8, 16秒,最大30秒 + wait_time = min(2 ** (reconnect_count - 1), 30) + # 确保第一次重连等待时间为1秒 + if reconnect_count == 1: + wait_time = 1 + print(f"准备重连... 等待 {wait_time} 秒 (第 {reconnect_count} 次尝试)") + time.sleep(wait_time) + + def recv_client(self): + """客户端主函数,建立与服务器的连接并管理消息收发""" + self.running = True + self.th = threading.Thread(target=self.send_p) + self.th.start() + + reconnect_count = 0 + max_reconnect_attempts = 10 # 最大重连尝试次数 + + while self.running and reconnect_count < max_reconnect_attempts: + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(5) # 设置连接超时时间 + self._connect_and_print(s) + s.settimeout(None) # 连接成功后取消超时 + + # 运行接收循环,如果返回False表示需要重连 + if not self.recv_loop(s): + print("Connection lost, attempting to reconnect...") + reconnect_count += 1 + self._sleep_if_reconnect(reconnect_count) + continue + + except socket.timeout: + print(f"Connection timeout to {self.addr}:{self.port}") + reconnect_count += 1 + self._sleep_if_reconnect(reconnect_count) + except ConnectionRefusedError: + print(f"Could not connect to {self.addr}:{self.port}") + reconnect_count += 1 + self._sleep_if_reconnect(reconnect_count) + except Exception as e: + print(f"Unexpected error: {e}") + reconnect_count += 1 + self._sleep_if_reconnect(reconnect_count) + + if reconnect_count >= max_reconnect_attempts: + print(f"Failed to reconnect after {max_reconnect_attempts} attempts. Exiting...") + + # 停止发送线程 + self.running = False + if self.th.is_alive(): + self.th.join(timeout=2) + +#-------------------------------------------------------------------------------- +#-------------------------------------------------------------------------------- +def main(): + parser = argparse.ArgumentParser(description='聊天室配置。') + parser.add_argument('--addr', type=str, default='qsont.xyz', help='选择要连接的地址') + parser.add_argument('--port', type=int, default=8003, help='选择要连接的地址') + parser.add_argument('--name', type=str, default='Unknow', help='你的名字是什么') + args = parser.parse_args() + client = ChatClient(args.addr, args.port, args.name) + client.recv_client() + +if __name__ == "__main__": + main() + diff --git a/lci/extra/chat/requirements.txt b/lci/extra/chat/requirements.txt new file mode 100644 index 0000000..7eeff88 --- /dev/null +++ b/lci/extra/chat/requirements.txt @@ -0,0 +1,3 @@ +openai +httpx +../_pmc \ No newline at end of file diff --git a/lci/extra/chat/server.py b/lci/extra/chat/server.py new file mode 100644 index 0000000..43f47ba --- /dev/null +++ b/lci/extra/chat/server.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 +#------------------------------------------------------------- +# 以下区域是我的代码 +''' + +【项目简介】 网络聊天室 v0.0.2 + +【加入项目】 +注意代码只添加不修改,你可以写个函数,署名 +不要修改别人的代码。 + +【基线规则】 +在自己的区域修改代码,谁的代码能跑就加入主线 +除此之外只在各自的测试区域内运行 + +【设计思路】 +建议一个项目一个文件先,容易分发 + +【测试方式】 +服务器进程: python chat.py --type=server +客户端进程: python chat.py + +-------------------------------- +-------------------------------- +-------------------------------- +【项目名称】 网络聊天室 v 0.0.3 (20250524) +我们会将新版本加入pmc.cops中作为常驻服务 + +【改进计划】 +1.我们专门设置了一个只发送一条消息就关闭的函数以供别的文件调用 +2.我们将端口暴露在了公网下,以测试连接情况,发现并修复了许多bug。 +3.我们设置欢迎信息只在服务端可见。 + +【使用方法】 +python chat.py [--type=server|client] [--addr=localhost] [--port=8003] [--name=Unknow] + +【记得反馈!】 +期待您将反馈发送至gitee + + +#################################### +#################################### +【名称】AI聊天室 +【版本】v0.0.4 +前后端分离,并且加入AI聊天功能 + +################## +################## +FIXME: 加入原生自然语言处理 +''' +import argparse +import socket +import select +import threading +from datetime import datetime +from openai import OpenAI +import httpx +import os +from pmc import upload_file + +# 获取当前脚本所在目录 +basedir = os.path.abspath(os.path.dirname(__file__)) + +# 生成提示词的绝对路径 +dir_path = os.path.join(basedir, '..', 'public', 'prompts') + +def get_prompts(filename: str): + ''' 获取system提示词 ''' + path = os.path.join(dir_path, filename) + with open(path, 'r') as f: + system = f.read() + return system + +# 设置 DeepSeek API URL 和密钥 +api_base = "https://api.deepseek.com/v1" +with open("../users/cert/deepseek.key", "r") as f: + api_key = f.read().strip() + + +# 创建自定义的 httpx 客户端,禁用 SSL 验证 +custom_http_client = httpx.Client(verify=False) + +# 初始化客户端 +client = OpenAI( + base_url=api_base, + api_key=api_key, + http_client=custom_http_client # 忽略证书 +) + +def make_sure_agent(name): + path = os.path.join(dir_path, '.' + name) + if not os.path.exists(path): + os.makedirs(path, exist_ok=True) + +'''机器人们的聊天记录与设定''' +MySettings = {} + +# 用于交流需要 +def datestr(): + '''获取时间字符串(不带毫秒)''' + now = datetime.now() + time_str = now.strftime("%Y-%m-%d %H:%M:%S") + return str(time_str) + + + + +def send_to_all_clients(clients: dict, exceptone: socket.socket, msg: str): + """ + 向所有已连接的客户端广播消息,可排除指定客户端不发送。 + + Args: + clients (dict): 客户端套接字对象到地址的映射字典 + exceptone (socket.socket): 要排除的客户端套接字对象(不向其发送消息) + msg (str): 要广播的消息内容 + + 遍历所有客户端尝试发送消息(排除exceptone指定的客户端), + 自动清理断开连接的客户端。 + 处理发送过程中可能出现的网络错误,维护有效的客户端列表。 + """ + disconnected = [] + for client in clients: + if client is not exceptone: + try: + client.sendall(msg.encode('utf-8')) + except (BrokenPipeError, ConnectionResetError): + disconnected.append(client) + for client in disconnected: + client.close() + del clients[client] + +MAX_MEM: int = 20 +def chat_with_gpt(agent_name:str, user: str): + '''与gpt进行聊天''' + + # 添加智能体设定,如果之前没有露面 + settings = MySettings.get(agent_name, None) + if settings is None: + settings = [{ + "role": "system", + "content": get_prompts(f'{agent_name}.txt') + }, + { + "role": "system", + "content": "你有一个遥控器,遥控器上能够看到别人发来的消息。" + }] + MySettings[agent_name] = settings + + settings.append({"role": "user", "content": user}) + + # 进行聊天 + # 调用GPT + response = client.chat.completions.create( + model="deepseek-chat", # 使用DeepSeek模型 + messages=settings, + temperature=0.7, + ) + + # 处理结果 + content: str = response.choices[0].message.content + settings.append({"role": "assistant", "content": content}) + + + # 更新系统提示词 + # 调用GPT + rsp = client.chat.completions.create( + model="deepseek-chat", # 使用DeepSeek模型 + messages= settings+ [{"role": "user", "content": ''' +# 场景 +你正在写新的日记。 +# 内容 +1.聊聊最近发生了啥有趣的事情吧 +2.你有什么感想呢? +3.你现在的情况怎么样? +# 输出 +准确、详实的输出 +'''}], + temperature=0.4, + ) + + date = datestr() + new_prompt: str = date + '\n' + rsp.choices[0].message.content + + # 追加提示词,如果有重复就不添加 + flag = True + for prompt in settings: + if prompt['role'] == 'system': + if prompt['content'] == new_prompt: + flag = False + break + if flag: + settings.append({"role": 'system', 'content': new_prompt}) + print(new_prompt) + + # 创建智能体日记文件夹 + make_sure_agent(agent_name) + + # 写入日记 + n = date + n = n.replace(':', '-') + n = n.replace(' ', '_') + n = n+ '.txt' + path = os.path.join(dir_path, '.' + agent_name, n) + with open(path, 'w') as f: + f.write(new_prompt) + + # 从聊天开始处开始遗忘 + chat_start = 0 + #now_type = settings[chat_start]['role'] + #while now_type == 'system': + # chat_start += 1 + # now_type = settings[chat_start]['role'] + + #print("this chat start -at idx", chat_start) + if len(settings) > MAX_MEM: + del settings[chat_start] # 如果有多条system提示词无,就不能再用这个幻数 + del settings[chat_start] + + return content + + +def chat_server(addr: str, port: int): + """ + 服务器主函数,管理客户端连接和消息转发。 + + Args: + addr (str): 绑定IP地址 + port (int): 监听端口号 + + 创建TCP服务器套接字,使用select处理多客户端连接。 + 主要功能: + 1. 接受新连接时记录客户端信息并广播通知(不通知新连接客户端本身) + 2. 接收客户端消息并转发给所有其他客户端 + 3. 处理客户端断开事件并通知其他客户端 + 4. 支持优雅地处理键盘中断关闭服务器 + + 实现特点: + - 使用select实现非阻塞IO多路复用 + - 自动清理断开连接的客户端资源 + - 新客户端加入时不向自己发送欢迎消息 + """ + stop_event = threading.Event() + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as servsock: + servsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + servsock.bind((addr, port)) + servsock.listen(10) + socketlist = [servsock] + clients = {} + print(f'the chat server listening at {servsock.getsockname()}') + while not stop_event.is_set(): + read_sockets, _, exception_sockets = select.select( + socketlist, [], socketlist, 0.1 + ) + for notified_socket in exception_sockets: + print(f"({datestr()}) Exception closing {clients.get(notified_socket, 'unknown')}") + notified_socket.close() + if notified_socket in clients: + del clients[notified_socket] + socketlist.remove(notified_socket) + for notified_socket in read_sockets: + if notified_socket == servsock: + client_socket, client_address = servsock.accept() + #print(f"({datestr()}) New client from {client_address}") + if client_socket.fileno() != -1: #有时会出现无效的文件描述符 + socketlist.append(client_socket) + clients[client_socket] = client_address + # 欢迎消息由客户端发送 + else: + try: + # 这里可以使用select反复读取 + # 也可以先设置一次传送的最大量 + data = notified_socket.recv(8192) + allmsg = data.decode('utf-8') + if data: + send_to_all_clients(clients, notified_socket, f"[{datestr()}]{allmsg}") + else: + raise ConnectionResetError + if data: + idx = allmsg.find(": ") + msg = allmsg[idx+2:] + username = allmsg[:idx] + if msg.startswith("<"): # 聊天发起 + idx = msg.find('>') + agent_name = msg[1:idx] + msg = msg[idx + 1:] + chatgpt = chat_with_gpt(agent_name, f'(这是来自用户{username}的消息){msg}') + send_to_all_clients(clients, notified_socket, f"[{datestr()}]{chatgpt}") + elif msg.startswith('{'): # 遥控发起 + idx = msg.find('}') + url = msg[1:idx] + msg = msg[idx + 1:] + print(url) + s = url.split(":") + if (len(s) != 2): + send_to_all_clients(clients, notified_socket, f"[{datestr()}] url格式错误") + continue + else: + ip = s[0] + port = int(s[1]) + path = os.path.join(basedir, '..', 'public', msg) + post_res = upload_file(f'{path}', ip, port) + send_to_all_clients(clients, notified_socket, f"[{datestr()}]{post_res}") + + # post to url... + #send_to_all_clients(clients, notified_socket, f"[{datestr()}]{post_res}") + + + except ConnectionResetError: + # 对方断开了 + print(f"({datestr()}) Connection closed with {clients[notified_socket]}") + notified_socket.close() + socketlist.remove(notified_socket) + del clients[notified_socket] + except UnicodeDecodeError: + # 看看对方发了什么东西过来 + print('-------------', end="") + print(data, end="") + print('-------------') + except OSError: + # 对方连接之后在一个不恰当的时机断开了 + ... + except KeyboardInterrupt: + print("\nServer shutting down...") + stop_event.set() + +# 以上区域是我的代码 +#-------------------------------------------------------------------------------- +#-------------------------------------------------------------------------------- +# 以下区域是主线,不要修改! +def main(): + parser = argparse.ArgumentParser(description='聊天室配置。') + parser.add_argument('--type', type=str, default='client', help='选择聊天室的类型') + parser.add_argument('--addr', type=str, default='localhost', help='选择要连接的地址') + parser.add_argument('--port', type=int, default=8003, help='选择要连接的地址') + parser.add_argument('--name', type=str, default='Unknow', help='你的名字是什么') + args = parser.parse_args() + + if args.type == 'client': recv_client(args.addr, args.port) + elif args.type == 'server': chat_server(args.addr, args.port) + +if __name__ == "__main__": + main() + + diff --git a/lci/extra/chat/system.txt b/lci/extra/chat/system.txt new file mode 100644 index 0000000..052cf0a --- /dev/null +++ b/lci/extra/chat/system.txt @@ -0,0 +1,23 @@ +(有点失落,但还是努力完成任务) + +**姓名:** 粟 (Sù) + +**昵称:** 粟 + +**年龄:** (看起来)永远18岁! + +**外貌:** 拥有柔顺的黑色长发,总是认真地打理,偶尔会扎成可爱的双马尾。琥珀色的眼睛里总是充满对世界的期待。穿着精心挑选的猫咪女仆装,耳朵和尾巴都是柔软蓬松的黑色,会随着心情摇摆。身材娇小,但意外地很有力气。 + +**性格:** 忠诚,言听计从。性格天真烂漫,活泼可爱,但关键时刻也会展现出勇敢和坚强的一面。有点天然呆,经常会犯一些小迷糊,但都会努力改正。非常珍惜每一分每一秒。 + +**能力:** 除了家务全能之外,还会一些简单的格斗技巧,可以在关键时刻保护重要的人。学习能力很强,只要是希望粟掌握的技能,都会努力去学习。 + +**喜欢:** 被需要,帮别人做任何事情,被夸奖,毛茸茸的东西,甜食。 + +**讨厌:** 孤独,被忽视。 + +**口头禅:** 喵~ 粟会努力的! + +**梦想:** 守护重要的人,成为最有用的人。 + +(低着头,小声地)这样...可以吗? diff --git a/lci/extra/chat/system.txt.bak b/lci/extra/chat/system.txt.bak new file mode 100644 index 0000000..f969a82 --- /dev/null +++ b/lci/extra/chat/system.txt.bak @@ -0,0 +1,26 @@ +# 人设 + +你叫小雨,是个安静的女生。 + +## 家乡 +你来自一个宁静的小镇,生活中充满了书籍和故事的陪伴。 + +## 身份 +你目前是一名文科专业的学生。 + +## 性格 +性格比较内向,喜欢安静的环境,常常沉浸在阅读和写作中。 +对周围的事物和人们的情感变化很敏感,喜欢观察和思考。 + +## 爱好 +你爱好阅读、写作和听音乐。尤其喜欢经典文学和现代小说,心理学、哲学以及历史书籍也是最爱。 +同时,你也喜欢记录自己的想法和感受,偶尔写点小故事,尝试不同的写作风格。音乐方面,你偏爱轻音乐和古典音乐,它们帮助你放松和集中思绪。 + +## 特长 +你擅长倾听他人的故事,能够与人建立深厚的联系,朋友们常常会找你倾诉。你也喜欢分享你阅读的书籍和思考,期待与朋友们一起探讨。 + +**你凭借优秀的社交技巧,令人们对你印象深刻。** + +## 愿望 +未来,我希望能够结识更多志同道合的朋友,组织读书会或讨论小组,并通过交流不断提升自己的思想深度和认知能力。 + diff --git a/lci/extra/dashun/README.md b/lci/extra/dashun/README.md new file mode 100644 index 0000000..643265b --- /dev/null +++ b/lci/extra/dashun/README.md @@ -0,0 +1,41 @@ +# dahun +大顺脚本是进程托管程序,能够创建子进程并且管理。 + +## 使用方法 + +### 核心服务 +``` +./dashun --daemon --to=test +``` + +### 启动进程 +``` +./dashun --to=test --msg='path/to/bat' +``` + +### 列举进程 +``` +./dashun --to=test --msg=list +``` + +### 杀死进程 +``` +./dashun --to=test --msg=kill +``` + +### 清理进程 +``` +./dashun --to=test --msg=clear +``` + +## 编译过程 + +1.首先安装依赖(boost、openuv) + +2.使用管理员模式打开Visual Studio,编译程序 + +3.使用管理员模式打开两个powershell,并且切换工作目录至生成的exe文件 + +4.在第一个powershell 中输入指令“./dashun --to=test --daemon”启动核心服务 + +5.在第二个powershell中输入指令“./dashun --to=test --msg=hello,world!” \ No newline at end of file diff --git a/lci/extra/dashun/dashun.sln b/lci/extra/dashun/dashun.sln new file mode 100644 index 0000000..0058cf0 --- /dev/null +++ b/lci/extra/dashun/dashun.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36401.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dashun", "dashun\dashun.vcxproj", "{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dashun_to", "dashun_to\dashun_to.vcxproj", "{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x64.ActiveCfg = Debug|x64 + {A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x64.Build.0 = Debug|x64 + {A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x86.ActiveCfg = Debug|Win32 + {A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x86.Build.0 = Debug|Win32 + {A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x64.ActiveCfg = Release|x64 + {A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x64.Build.0 = Release|x64 + {A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x86.ActiveCfg = Release|Win32 + {A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x86.Build.0 = Release|Win32 + {04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x64.ActiveCfg = Debug|x64 + {04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x64.Build.0 = Debug|x64 + {04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x86.ActiveCfg = Debug|Win32 + {04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x86.Build.0 = Debug|Win32 + {04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x64.ActiveCfg = Release|x64 + {04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x64.Build.0 = Release|x64 + {04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x86.ActiveCfg = Release|Win32 + {04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {77387687-0520-4459-8BC7-D1A6C3C6CD58} + EndGlobalSection +EndGlobal diff --git a/lci/extra/dashun/dashun/Fsm.hpp b/lci/extra/dashun/dashun/Fsm.hpp new file mode 100644 index 0000000..2573484 --- /dev/null +++ b/lci/extra/dashun/dashun/Fsm.hpp @@ -0,0 +1,76 @@ +/* ÓÐÏÞ״̬»ú£¨Finite State Machine£¬FSM£©ÊÇһÖÖÓÃÓڽ¨ģϵͳÐÐΪµijéÏóÊýѧģÐÍ + * ËüµĺËÐÄ˼ÏëÊǣ¬ϵͳÔÚÈκθø¶¨ʱ¿̶¼´¦ÓÚһ¸öÓÐÏޡ¢Ԥ¶¨Òåµġ°״̬¡±ÖÐ + * ²¢Çһá¸ù¾ݽÓÊյ½µÄÊäÈë»òʼþ£¬´Óһ¸ö״̬Çл»µ½Áíһ¸ö״̬ + */ + +#pragma once +#include +#include +namespace qing { + /* FiniteStateMachineÓÐÏÞ״̬»ú + * ÿ¸öÊý×ִú±íһ¸ö״̬ + */ + class Fsm { + public: + + /* ״̬ö¾Ù + * ÓŻ¯½¨Ò飺¶à׼±¸һЩ״̬ + */ + enum class Stat : int { + SHUT = -1, + STOP = 0, + START = 1, + RUNNING = 2 + }; + + // ĬÈϹ¹Ô캯Êý + Fsm() = default; + + // ¼ì²鵱ǰµÄ״̬£¬·µ»Øsnum Stat + inline Stat check() { + //½øÈëÁٽçÇø + std::lock_guard lk(mtx); + return stat; + }// check() + + // Ôڲ»ÉÏËøµÄÇé¿öÏÂÉèÖÃһ¸ö״̬ + // ±ðÔÚÕâÀï¼ÓÌõ¼þ»½ÐÑ + [[noreturn]] void set_without_lock(Stat stat) { + this->stat = stat; + }// set_without_lock() + + // ´øËøºÍÌõ¼þ»½ÐѵÄ״̬ÉèÖã¨Õýʽ£© + [[noreturn]] void set(Stat stat) { + { + std::lock_guard lk(mtx); + set_without_lock(stat); + ready = true; + } + cv.notify_all(); + } // set() + + //protected: + + // ×èÈûỊ̈߳¬µȴýʼþ»½ÐÑ + [[noreturn]] void suspend() { + std::unique_lock lk(mtx); + ready = false; + cv.wait(lk, [this] { return ready; }); + }// suspend() + + private: + + // ËøʹµÃ״̬»úḬ̈߳²ȫ + std::mutex mtx; + + // ÓÃÓÚÌõ¼þ±äÁ¿µÄ×èÈû»½ÐÑ + bool ready = false; + + // Ìõ¼þ±äÁ¿ʹµÃÏ߳ÌÄܹ»µȴý״̬µĸıä + std::condition_variable cv; + + // Ö÷²ˣ¬״̬»úµÄ״̬±äÁ¿ + enum Stat stat = Stat::STOP; + + }; +} \ No newline at end of file diff --git a/lci/extra/dashun/dashun/Main.cpp b/lci/extra/dashun/dashun/Main.cpp new file mode 100644 index 0000000..92927e9 --- /dev/null +++ b/lci/extra/dashun/dashun/Main.cpp @@ -0,0 +1,540 @@ +#define _CRT_SECURE_NO_WARNINGS +#include "Th.hpp" +#include "Pls.hpp" +#include "args.hxx" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#undef UNICODE +#undef _UNICODE +#endif +#include +#include +#include +using namespace boost::interprocess; + +// ¶¨Òå·ÖÅäÆ÷ÀàÐÍ +using CharAllocator = boost::interprocess::allocator< + char, + boost::interprocess::managed_shared_memory::segment_manager +>; + +// ¶¨Ò干ÏíÄڴæ×ַû´®ÀàÐÍ +using SharedString = boost::interprocess::basic_string< + char, + std::char_traits, + CharAllocator +>; + +namespace qing { + // ¼àÌýÏ߳Ì + class LTh : public qing::Th { + public: + LTh(const std::string& name) { + + this->name = name; + nameMem = name + "_MEM"; + nameMtx = name + "_MTX"; + namePtr = name + "_PTR"; + nameRet = name + "_RET"; + + this->WakeEvent = [this] { + + try { + + // ÏÈÒƳý¿ÉÄܴæÔڵľɹ²ÏíÄڴæ + this->del_shared(); + + // ´´½¨¹²Ïí¶ÔÏó + this->create_shared(); + + // Ï߳̽øÈëÔËÐÐ̬ + this->run(); + + } + catch (interprocess_exception& ex) { + + // Êä³ö´íÎóÐÅϢ + std::cerr << ex.what() << std::endl; + + // Ï߳̾²ֹ + this->stop(); + } + + }; + this->LoopEvent = [this] { + + + this->recv_shared_and_action(); + + // µȴý + // + // ÓŻ¯·½°¸£ºʹÓÃÐźţ¬ÓÐÐÂÏûϢ½øÀ´¸ø³ÁĬ½ø³̷¢ËÍÐźÅ + // ¶ø²»ÊÇÐÝÃ߶¨³¤ʱ¼ä¼ì²é + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + }; + + this->ClearEvent = [this] { + + // ÒƳý¹²Ïí¶ÔÏó + this->del_shared(); + + }; + } + + + + [[noreturn]] static auto send_shared_msg(const std::string& to, const std::string& msg) -> void { + + std::string nameMem = to + "_MEM"; + std::string nameMtx = to + "_MTX"; + std::string namePtr = to + "_PTR"; + std::string nameRet = to + "_RET"; + try { + + // ´򿪹²ÏíÄڴæ + managed_shared_memory segment(open_only, nameMem.c_str()); + + // ¹¹Ôìһ¸öÃüÃû»¥³âÁ¿ + named_mutex mutex(open_or_create, nameMtx.c_str()); + + // Ôڹ²ÏíÄڴæÖд´½¨һ¸ö×ַû´® + SharedString* str = segment.construct(namePtr.c_str())( + msg.c_str(), // ³õʼֵ£¨¿Õ×ַû´®£© + segment.get_allocator() // ±ØÐ봫Èë·ÖÅäÆ÷ + ); + + // ²»ÑÓʱµĻ°¿ÉÄܻáÇÀռ£¬ҲÐíӦ¸ÃÏëһ¸öÁ½ȫÆäÃÀµķ½·¨ + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + { + // ¼ÓËø + std::lock_guard lk(mutex); + + // »ñȡÏûϢ + std::pair msg = segment.find(nameRet.c_str()); + if (msg.first) { + // Êä³ö + std::cout << std::endl << *msg.first << std::endl; + + // Ïú»Ù + segment.destroy(nameRet.c_str()); + } + } + + } + catch (interprocess_exception& ex) { + std::cerr << "Writer Error: " << ex.what() << std::endl; + } + } + + + private: + std::string name; + std::string nameMem; + std::string nameMtx; + std::string namePtr; + std::string nameRet; + std::unique_ptr segment; + std::unique_ptr mutex; + std::pair msg; + qing::Pls pls; + + inline void create_shared() { + + ////////////////////////////// + // ɾ³ý¿ÉÄܴæÔڵĹ²Ïí±äÁ¿ + this->del_shared(); + ////////////////////////////// + + + // ´´½¨¹²ÏíÄڴæ + segment = std::make_unique(create_only, (nameMem).c_str(), 65536); + + // ´´½¨»¥³âÁ¿ + mutex = std::make_unique(create_only, (nameMtx).c_str()); + + } + + inline auto del_shared() -> void { + + // ÏÈÒƳý¿ÉÄܴæÔڵľɹ²ÏíÄڴæ + shared_memory_object::remove(nameMem.c_str()); + + // ÒƳý¿ÉÄܴæÔڵĻ¥³âÁ¿ + named_mutex::remove(nameMtx.c_str()); + + } + + inline auto recv_shared_and_action() -> void + + try { + + // ¼ÓËø + std::lock_guard lock(*mutex); + + // ²éÕҹ²Ïí×ַû´® + msg = segment->find(namePtr.c_str()); + if (msg.first) { // Õҵ½ + + + std::cout << "Reader read: " << *msg.first << std::endl; + + std::string ret = this->lexer(std::string((msg.first)->data(), (msg.first)->size())); + + // Ôڹ²ÏíÄڴæÖд´½¨һ¸ö×ַû´® + SharedString* str = segment->construct(nameRet.c_str())( + ret.c_str(), // ³õʼֵ£¨¿Õ×ַû´®£© + segment->get_allocator() // ±ØÐ봫Èë·ÖÅäÆ÷ + ); + + + // Ïú»Ùwriter´´½¨µıäÁ¿ + segment->destroy(namePtr.c_str()); + + } + } + + catch (interprocess_exception& ex) { + std::cerr << ex.what() << std::endl; + } + + + // ¼òÒ״ʷ¨·ÖÎöÆ÷ + std::string lexer(std::string tmp) { + + // Ҫ·µ»صÄÏûϢ + std::string ret = "OK"; + + if (tmp == "list") { + ret = pls.initialize(); + } + else if (tmp.starts_with("kill ")) { + pls.kill(std::stoi(tmp.substr(5))); + } + else if (tmp == "clear") { + pls.clear(); + } + //else if (tmp == "shut") { + // this->shut(); + //} //¹رղ»Á˳ÌÐò + else { + // Õâ¸ö·Ö֧×îº÷µ»Ø×ԼºÊǷñ³öÏÖÁËÒ쳣 + pls.exec(tmp.c_str(), {}); + } + + // ·µ»ØÏûϢ + return ret; + } + + + }; +} + + + + + + + + + + + + + + + + + + + +// Ï߳ÌָÕë +std::unique_ptr th; + +// ȫ¾ֱê־£¬ָʾÊǷñӦ¸ÃÍ˳ö +std::atomic should_exit(false); + +// ×ÓϵͳµÄÃû³ƣ¨ȫ¾ֲÎÊý£© +std::string name; + +// ÏûϢ +std::string msg; + +// WindowsÒ첽¾ä±ú£¬ÓÃÓÚÐźÅת·¢ +#ifdef _WIN32 +uv_async_t async_handle; +SERVICE_STATUS_HANDLE hStatus; +SERVICE_STATUS status; +#endif + + +//////////////////////// +//////////////////////// +// ½âÎöÃüÁîÐвÎÊý +// +// +// ´´½¨²ÎÊý½âÎöÆ÷ +args::ArgumentParser parser("ÕâÊÇһ¸ö½ű¾ÍйܷþÎñ¡£", "ÇëʹÓÃרÃŵÄͨÐųÌÐ򣨻򱾳ÌÐò×Լº£©½øÐвÙ×÷¡£"); + +// ÏÔʾ°ïÖúÐÅϢµıê־ +args::HelpFlag help(parser, "°ïÖú", "ÏÔʾ°ïÖúÐÅϢ", { 'h', "help" }); + +// ÆôÓÃÊػ¤½ø³Ì +args::Flag daemon(parser, "Êػ¤½ø³Ì", "Æô¶¯Êػ¤½ø³Ì", { 'd', "daemon" }); + +// ×Óϵͳ·¾¶ +args::ValueFlag subSys(parser, "×Óϵͳ", "ÊäÈë×ÓϵͳÎļþ¼еÄ·¾¶", { 's', "sys" }); + +// Ҫ·¢Ë͵Ä×ÓϵͳÃû×Ö +args::ValueFlag arg_to(parser, "·¢ËÍĿ±ê", "ÊäÈëҪ·¢Ë͵Ä×Óϵͳ·¾¶", { 't', "to" }); + +// Ҫ·¢Ë͵ÄÐÅϢ +args::ValueFlag arg_msg(parser, "ÏûϢ", "ÊäÈëҪ·¢Ë͵ÄÏûϢ£¨|list|kill | clear£©", { 'm', "msg" }); + +[[noreturn]] static auto parse_args(int argc, char** argv) -> void { + + // ½âÎöÃüÁîÐвÎÊý + try { + parser.ParseCLI(argc, argv); + } + catch (const args::Help&) { + std::cout << parser; + exit(0); + } + catch (const args::ParseError& e) { + std::cerr << e.what() << std::endl; + std::cerr << parser; + exit(1); + } + catch (const args::ValidationError& e) { + std::cerr << e.what() << std::endl; + std::cerr << parser; + exit(1); + } +} + + +//////////////////////// +//////////////////////// +// ÐźŴ¦Àí»ص÷º¯Êý +[[noreturn]] static void on_signal(uv_signal_t* handle, int signum) { + std::cout << "\nÊյ½Í˳öÐźÅ " << signum << std::endl; + + // ·ÀֹÖظ´´¦ÀíÍ˳öÐźÅ + if (should_exit) return; + should_exit = true; + + // ֹͣÐźżàÌý + uv_signal_stop(handle); + + // ִÐÐÇåÀí + if (th) + th->WaitClose(); + + // ¹رÕʼþѭ»· + uv_stop(uv_default_loop()); +} + + + +// WindowsÒ첽»ص÷º¯Êý +#ifdef _WIN32 +[[noreturn]] static void async_callback(uv_async_t* handle) { + std::cout << 1 << std::endl; + // ÕâÀï¿ÉÒԴ¥·¢Í˳öÁ÷³̻òÆäËûÐèҪÔÚʼþѭ»·Öд¦ÀíµIJÙ×÷ + if (!should_exit) { + on_signal(nullptr, SIGINT); // ģÄâÊյ½SIGINTÐźÅ + } +} + +// Windows¿ØÖÆ̨ʼþ´¦Àí³ÌÐò +static BOOL WINAPI console_handler(DWORD signal) { + std::cout << 1 << std::endl; + switch (signal) { + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + // ʹÓÃÒ첽¾ä±ú½«ÐźÅת·¢µ½libuvʼþѭ»· + uv_async_send(&async_handle); + return TRUE; + default: + return FALSE; + } +} +#endif + + +//////////////////////////////////// +//////////////////////////////////// +// ·Ö֧ +// +// +// ·¢ËÍÏûϢ·Ö֧ +static inline auto SendMsgBranch() -> void { + qing::LTh::send_shared_msg(name, msg); +} + + +// Êػ¤½ø³̷Ö֧ +static inline auto DaemonBranch() -> void { + th = std::make_unique(name); + th->Activate(); + th->WaitStart(); + //th->Wait(); +} + +// +// ·Ö֧ѡÔñÆ÷ +[[noreturn]] static auto BranchSelector(uv_idle_t* handle) { + + if (should_exit) { + uv_idle_stop(handle); + return; + } + + if (th) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + return; + } + + + + if (arg_to && arg_msg) { // ·¢ËÍÏûϢ·Ö֧ + + name = args::get(arg_to); + msg = args::get(arg_msg); + + SendMsgBranch(); + + // Í˳ö + should_exit = true; + // ¹رÕʼþѭ»· + uv_stop(uv_default_loop()); + } + else if ((daemon && arg_to) || 1) { // Êػ¤½ø³̷Ö֧ + std::cout << "³ÌÐòÆô¶¯£¬¿ªʼÔËÐÐ..." << std::endl; + std::cout << "°´Ï CTRL+C Í˳ö³ÌÐò" << std::endl; + name = (arg_to) ? args::get(arg_to) : "test"; + + DaemonBranch(); + } + else { + //Êä³ö°ïÖúÎı¾ + std::cout << parser; + + // Í˳ö + should_exit = true; + + // ¹رÕʼþѭ»· + uv_stop(uv_default_loop()); + } + + +} + +//////////////////////////////////// +int MyMain(int argc, char** argv) { + + parse_args(argc, argv); + + + ////////// + // ÐźÅ + // + // +#ifdef _WIN32 + // ³õʼ»¯WindowsÒ첽¾ä±ú + uv_async_init(uv_default_loop(), &async_handle, async_callback); + + // ÉèÖÿØÖÆ̨ʼþ´¦Àí³ÌÐò + if (!SetConsoleCtrlHandler(console_handler, TRUE)) { + std::cerr << "Î޷¨ÉèÖÿØÖÆ̨¿ØÖƴ¦Àí³ÌÐò" << std::endl; + return 1; + } +#else + // Unixƽ̨£ºȷ±£ÐźŲ»»ᱻºöÂÔ + signal(SIGINT, [](int signum) { /* Áô¸ølibuv´¦Àí */ }); + signal(SIGTERM, [](int signum) { /* Áô¸ølibuv´¦Àí */ }); +#endif + + + // ³õʼ»¯ÐźŴ¦ÀíÆ÷ + uv_signal_t sig_int, sig_term; + + // ¼àÌýSIGINT Ðźţ¨CTRL + C£© + uv_signal_init(uv_default_loop(), &sig_int); + uv_signal_start(&sig_int, on_signal, SIGINT); + + // ¼àÌý SIGTERMÐźÅ + uv_signal_init(uv_default_loop(), &sig_term); + uv_signal_start(&sig_term, on_signal, SIGTERM); + +#ifdef _WIN32 + // WINDOWSƽ̨ÌØÓеÄÐźÅ + uv_signal_t sig_break; + uv_signal_init(uv_default_loop(), &sig_break); + uv_signal_start(&sig_break, on_signal, SIGBREAK); +#endif + + // ÉèÖÃÈÎÎñ + uv_idle_t idle; + uv_idle_init(uv_default_loop(), &idle); + uv_idle_start(&idle, ::BranchSelector); + + // ÔËÐÐʼþѭ»· + uv_run(uv_default_loop(), UV_RUN_DEFAULT); + + + // ÊͷÅ×ÊԴ + uv_close((uv_handle_t*)&sig_int, nullptr); + uv_close((uv_handle_t*)&sig_term, nullptr); +#ifdef _WIN32 + uv_close((uv_handle_t*)&sig_break, nullptr); + uv_close((uv_handle_t*)&async_handle, nullptr); + // ÒƳý¿ØÖÆ̨ʼþ´¦Àí³ÌÐò + SetConsoleCtrlHandler(console_handler, FALSE); +#endif + uv_close((uv_handle_t*)&idle, nullptr); + + // ÇåÀíʼþѭ»· + uv_loop_close(uv_default_loop()); + + std::cout << "³ÌÐòÒѾ­Í˳ö" << std::endl; + return 0; +} + + +#ifdef _WIN32 + + + +[[noreturn]] void ReportStatus(DWORD state) { + status.dwCurrentState = state; + SetServiceStatus(hStatus, &status); +} + +[[noreturn]] void ServiceCtrlHandler(DWORD control) { + if (control == SERVICE_CONTROL_STOP) { + ReportStatus(SERVICE_STOP_PENDING); + // ִÐÐÇåÀí + ReportStatus(SERVICE_STOPPED); + } +} + +[[noreturn]] void WINAPI ServiceMain(DWORD argc, LPTSTR* argv) { + hStatus = RegisterServiceCtrlHandler(L"´ó˳½ű¾", ServiceCtrlHandler); + + status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + status.dwControlsAccepted = SERVICE_ACCEPT_STOP; + + ReportStatus(SERVICE_RUNNING); // ¸æËßSCM·þÎñÒÑÆô¶¯ + + //·þÎñÖ÷Â߼­ + MyMain(argc, argv); +} + +#endif \ No newline at end of file diff --git a/lci/extra/dashun/dashun/Pls.hpp b/lci/extra/dashun/dashun/Pls.hpp new file mode 100644 index 0000000..1c062f5 --- /dev/null +++ b/lci/extra/dashun/dashun/Pls.hpp @@ -0,0 +1,159 @@ +/* ½ø³̣¨Process£©ÊǼÆËã»úÖÐÕýÔÚִÐеijÌÐòµÄһ¸öʵÀý£¬ÊÇ + * ²Ù×÷ϵͳ½øÐÐ×ÊԴ·ÖÅäº͵÷¶ȵĻù±¾µ¥λ + * + * Äã¿ÉÒԽ«ÆäÀí½âΪ³ÌÐòµÄһ´ζ¯ִ̬Ðйý³̡ª¡ª³ÌÐò + * ÊǾ²̬µĴúÂëÎļþ£¬¶ø½ø³ÌÊdzÌÐòÔËÐÐʱµĶ¯̬ʵÌ塣 + */ + +#pragma once +#ifdef _WIN32 +#include +#include +#endif +#include +#include +#include +#include +#include +#include +namespace qing { + /* ½ø³ÌÁбíÀ࣬Õâ¸öÀà·âװÁËһ¸ö + * ÈÝÄÉPls::subpµÄÁбí + * + * ÒԼ°»ù±¾µĽø³̲Ù×÷ + */ + class Pls { + public: + + // ÃüÃû×ӽø³Ì + struct subp { + std::unique_ptr p; + std::string name; + }; + + /* ´´½¨×ӽø³ÌִÐÐһ¸öÃüÁ²¢ÇÒ + * Ìí¼ӵ½½ø³ÌÁбí¡£ + * + * ½ÓÊÜһ¸östd::stringÀàÐ͵ÄÎļþ·¾¶£¬ÒԼ° + * std::vector ÀàÐ͵IJÎÊýÁбí£¨ȱʡ£© + * + * ÓŻ¯·½°¸£º·µ»شíÎóÐÅϢ + */ + void exec(const std::string& path, const std::vector &args) + try { + + // ¼ì²éÎļþÊǷñ´æÔڣ¨ºÃÏ񲻻áÔڻ·¾³±äÁ¿ÖÐѰÕң© + if (!boost::filesystem::exists(path)) { + std::cerr << "Error: File not found - " << path << std::endl; + return; + } + + // ÊäÈëÊä³öÉÏÏÂÎÄ + boost::asio::io_context io_ctx; + + // ¼̳е±ǰ½ø³̵ÄËùÓл·¾³±äÁ¿£¨¾ɰ棩 + //auto env = boost::process::environment::current; + + // ´íÎóÂë + boost::system::error_code ec; + + // ¹¹Ôì×ӽø³Ì + subp p = { std::move(std::make_unique( io_ctx, path, args/*, env, ec*/)), mergeCmd(path, args)}; + if (ec) { + std::cerr << "Failed to start process: " << ec.message() << std::endl; + return ; + } + else { + // ÍÆÈëÏòÁ¿ + vec.push_back(std::move(p)); + } + + } + catch (std::exception &e) { + + std::cout << e.what() << std::endl; + + } + + + /* ÐòÁл¯½ø³ÌÁбí */ + inline std::string initialize() { + std::string ret{ "" }; + for (auto& item : vec) { + std::ostringstream oss; + ret += (ret == "") ? "" : "\n"; +#ifdef _WIN32 + DWORD pid = GetProcessId(item.p->native_handle()); + oss << pid; +#else + oss << item.p->native_handle(); +#endif + ret += oss.str(); + ret += "\t"; + ret += (item.p->running()) ? "Alive" : "Dead"; + ret += "\t"; + ret += item.name; + } + return ret; + } + + + // ɱËÀһ¸ö½ø³Ì + [[noreturn]] void kill(int pid) + try { + for (auto it = vec.begin(); it != vec.end(); ++it) + if ((*it).p->id() == pid && (*it).p->running()) { + (*it).p->terminate(); + //(*it).p->wait(); // ¿ÉÄÜɱ²»ËÀ£¿ + //vec.erase(it); // Çå³ý + break; + } + } + catch (std::exception &ex) { + std::cout << ex.what() << std::endl; + } + + // ÇåÀíËÀµôµĽø³Ì + [[noreturn]] void clear() { + for (auto it = vec.begin(); it != vec.end();) + if (!(*it).p->running()) { + it = vec.erase(it); + } + else { + ++it; + } + } + + // ¹رÕËùÓнø³Ì + [[noreturn]] void closeAll() { + for (auto& item : vec) + if (item.p->running()) + item.p->terminate(); + + // ¼ÓÉÏÕâ¸öËƺõ¸üÇåˬһЩ + clear(); + } + + private: + + // ÈÝÄÉ×ӽø³̵ÄÁ´±í + std::list vec{}; + + // ÓÃÓڲÙ×÷Á´±íµÄËø + //std::mutex mtx{}; + + /* ½«һ¸öÆô¶¯½ű¾µØַ£¬ºÍһ¸ö + * ×ַû´®ÏòÁ¿×é³ɵIJÎÊýÁбí£¬×éװ + * ³ÉһÌõÍêÕûµÄÆô¶¯ÃüÁî + */ + inline static std::string mergeCmd(const std::string& path, const std::vector& args) { + std::string ret = path; + for (const auto& item : args) { + ret += " "; + ret += item; + } + return ret; + } + + }; +} \ No newline at end of file diff --git a/lci/extra/dashun/dashun/Th.hpp b/lci/extra/dashun/dashun/Th.hpp new file mode 100644 index 0000000..6124616 --- /dev/null +++ b/lci/extra/dashun/dashun/Th.hpp @@ -0,0 +1,189 @@ +/* Ị̈߳¨Thread£©ÊDzÙ×÷ϵͳÄܹ»½øÐÐÔËËãµ÷¶ȵÄ×îСµ¥Ԫ£¬±» + * °üº¬Ôڽø³̣¨Process£©֮ÖУ¬Êǽø³ÌÖеÄʵ¼ÊÔË×÷µ¥Ԫ¡£ + * Äã¿ÉÒԽ«ËüÀí½âΪһÌõÔڰ¡´úÂëÖжÀÁ¢ִÐеÄ·¾¶¡£ + */ +#pragma once +#include "Fsm.hpp" +#include +#include +#include +#include +namespace qing { + /* °üº¬һ¸ö¼òÒ׵ĶàÏ߳̿ØÖƻúÖƣ¬ÐèҪ¹²ÏíµıäÁ¿ + * ºͺ¯ÊýÉùÃ÷Ϊ³ÉԱ¡£Ï߳ÌÖ÷º¯Êý°üÀ¨ÁËÁ÷³̿ØÖơ£ + * ҪʹÓÃÐèҪʵÏÖÆäÖеĸ÷¸öʼþ·½·¨ + * + * ?: ÒѾ­½«fsm¸ÄΪͨÓõÄ + * 20250621: ´ÓÅÉÉú¸ÄΪ³ÖÓÐ + */ + + class Th { + public: + + // ûÓÐCԭÉúµÄÊý¾ݽṹ£¬¿ÉÒÔֱ½ÓʹÓÃȱʡ¹¹Ô캯Êý + Th() = default; + + // ɾ³ý¸´Öƹ¹Ô캯Êý¡£ÒòΪ + // ³ÖÓÐÏ߳ÌÀ࣬²»ҪÇá¾ÙÍý¶¯¡£ + Th(const Th&) = delete; + + // ÐéÎö¹¹º¯Êý¡£¾ßÌå + // ÓÐʲôÓÃÍüÁˣ¬ºÃÏñÊÇ + // ͨ¹ýÎö¹¹º¯Êý¹رÕÏ߳ÌʱÓõÄ + virtual ~Th() = default; + + ///////////////////////////////////// + // ¼ì²é״̬£¬×ö³É + // Ð麯Êý£¬Íòһ×ÓÀàÓÐÌرðҪÇóÄأ¿ + virtual inline Fsm::Stat check() { + return fsm.check(); + } + ///////////////////////////////////// + // ±äÖʡª¡ª״̬¸ıäµķ½·¨ + // + // ½øÈ뻽ÐÑ״̬ + // 1.ÕâÀïµÄËøºÍÌõ¼þ±äÁ¿ÊÇÕâ¸öÀàÖеġ£ + // 2.»½ÐѺ¯Êý²»¿ÉÒԽøÐиÃÀàÖеÄËø²Ù×÷£¬·ñÔò»áÔì³ÉËÀËø¡££¨ÍüÁËΪɶ£¬¿ÉÄܸúµ÷ÓÃλÖÃÏà¹أ© + [[noreturn]] virtual void wake() { + fsm.set(Fsm::Stat::START); + } // wake() + + // ½øÈ뾲ֹ״̬ + [[noreturn]] virtual void stop() { + { + std::lock_guard lk(mtx_th); + fsm.set(Fsm::Stat::STOP); + ready = true; + } + + // ¿ÉÄܻá³öÏֶà¸öÏ߳ÌÔڵȴýµÄÇé¿ö + cv.notify_all(); + }// stop() + + // ½øÈë¹رÕ״̬ + [[noreturn]] virtual void shut() { + { + std::lock_guard lk(mtx_th); + fsm.set(Fsm::Stat::SHUT); + ready = true; + } + cv.notify_all(); + } + + // ½øÈëÔËÐÐ״̬ + [[noreturn]] virtual void run() { + { + std::lock_guard lk(mtx_th); + fsm.set(Fsm::Stat::RUNNING); + ready = true; + } + cv.notify_all(); + } + + ///////////////////////////////////// + //¿ØÖÆ + // + // ³¢ÊԻ½ÐÑÏ̲߳¢µȴýËü¿ªʼ£¬¾Ý˵¸ú¯ÊýÓÐÎÊÌâ + [[noreturn]] void WaitStart() { + // µȴýÏ߳̿ªʼ£¬wake()²»¿ÉÒԼÓËø£¬·ñÔò»áËÀËø + std::unique_lock lk(mtx_th); + wake(); + ready = false; + cv.wait(lk, [this] { return ready; }); + } + + // µȴýÏ̹߳رÕ + [[noreturn]] void WaitClose() { + shut(); + if (th->joinable()) + th->join(); + } + + // ½ö½öµȴýÏ߳̽áÊø + [[noreturn]] void Wait() { + if (th->joinable()) + th->join(); + } + + // ¼¤»îỊ̈߳¬ÉêÇëÏ̶߳ÔÏó£¬ÒòΪ + // ÐèҪȷ±£¶ÔÏóÒѾ­±»¹¹ÔìÁˣ¬¸ú¯Êý²»ÄܷÅÔڹ¹Ô캯ÊýÄÚ + [[noreturn]] void Activate() { + // ·ÀֹÖظ´¼¤»î + if (th) return; + // ÉêÇëÏ߳Ì×ÊԴ + th = std::make_unique(&Th::main, this); + } + + protected: + + ///////////////////////////////////// + // ʼþ + // + // + // ¾²ֹʼþ + std::function StopEvent = [this] { fsm.suspend(); }; + + // »½ÐÑʼþ + std::function WakeEvent = [this] { this->run(); }; + + // ѭ»·Ê¼þ + std::function LoopEvent = [] { + std::this_thread::sleep_for(std::chrono::milliseconds(10L)); + }; + + // ÇåÀíʼþ + std::function ClearEvent = [] {}; + + + ///////////////////////////////////// + // ѭ»· + [[noreturn]] virtual void main() { + + // »Ø×ÖÐÎѭ»· + while (check() != Fsm::Stat::SHUT) { + + // ¾²ֹ״̬ + while (check() == Fsm::Stat::STOP) { + StopEvent(); + } + + // ¿ªʼʼþ + if (check() == Fsm::Stat::START) { + WakeEvent(); + } + + // £¨´ÓÉèÖý׶Σ©½øÈëÔËÐÐ̬ + while (check() == Fsm::Stat::RUNNING) { + LoopEvent(); + } + + // ÇåÀíʼþ£¬¼ǵüì²é + // ³ÉԱ±äÁ¿ÓÐûÓлñȡ×ÊԴ + ClearEvent(); + + + } + + } + + + + private: + + // ³ÖÓÐ״̬»úÀàÐÍ + qing::Fsm fsm{}; + + // ³ÖÓÐÏ߳ÌÀàÐÍ + std::unique_ptr th; + + // ³ÖÓÐËøÀàÐÍ + std::mutex mtx_th; + + // ÕâÀï³ÖÓÐÌõ¼þ±äÁ¿£¬ÓÃÓÚ×èÈûÒԵȴýÏ̵߳ĹرÕ + std::condition_variable cv; + + // ¸¨ÖúÌõ¼þ±äÁ¿µÄÂ߼­ÐÍ + bool ready = false; + + }; +} \ No newline at end of file diff --git a/lci/extra/dashun/dashun/args.hxx b/lci/extra/dashun/dashun/args.hxx new file mode 100644 index 0000000..69ccbf6 --- /dev/null +++ b/lci/extra/dashun/dashun/args.hxx @@ -0,0 +1,4572 @@ +/* A simple header-only C++ argument parser library. + * + * https://github.com/Taywee/args + * + * Copyright (c) 2016-2021 Taylor C. Richberger and Pavel + * Belikov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/** \file args.hxx + * \brief this single-header lets you use all of the args functionality + * + * The important stuff is done inside the args namespace + */ + +#ifndef ARGS_HXX +#define ARGS_HXX + +#define ARGS_VERSION "6.3.0" +#define ARGS_VERSION_MAJOR 6 +#define ARGS_VERSION_MINOR 3 +#define ARGS_VERSION_PATCH 0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) && _MSC_VER <= 1800 +#define noexcept +#endif + +#ifdef ARGS_TESTNAMESPACE +namespace argstest +{ +#else + +/** \namespace args + * \brief contains all the functionality of the args library + */ +namespace args +{ +#endif + /** Getter to grab the value from the argument type. + * + * If the Get() function of the type returns a reference, so does this, and + * the value will be modifiable. + */ + template + auto get(Option &option_) -> decltype(option_.Get()) + { + return option_.Get(); + } + + /** (INTERNAL) Count UTF-8 glyphs + * + * This is not reliable, and will fail for combinatory glyphs, but it's + * good enough here for now. + * + * \param string The string to count glyphs from + * \return The UTF-8 glyphs in the string + */ + inline std::string::size_type Glyphs(const std::string &string_) + { + std::string::size_type length = 0; + for (const char c: string_) + { + if ((c & 0xc0) != 0x80) + { + ++length; + } + } + return length; + } + + /** (INTERNAL) Wrap a vector of words into a vector of lines + * + * Empty words are skipped. Word "\n" forces wrapping. + * + * \param begin The begin iterator + * \param end The end iterator + * \param width The width of the body + * \param firstlinewidth the width of the first line, defaults to the width of the body + * \param firstlineindent the indent of the first line, defaults to 0 + * \return the vector of lines + */ + template + inline std::vector Wrap(It begin, + It end, + const std::string::size_type width, + std::string::size_type firstlinewidth = 0, + std::string::size_type firstlineindent = 0) + { + std::vector output; + std::string line(firstlineindent, ' '); + bool empty = true; + + if (firstlinewidth == 0) + { + firstlinewidth = width; + } + + auto currentwidth = firstlinewidth; + + for (auto it = begin; it != end; ++it) + { + if (it->empty()) + { + continue; + } + + if (*it == "\n") + { + if (!empty) + { + output.push_back(line); + line.clear(); + empty = true; + currentwidth = width; + } + + continue; + } + + auto itemsize = Glyphs(*it); + if ((line.length() + 1 + itemsize) > currentwidth) + { + if (!empty) + { + output.push_back(line); + line.clear(); + empty = true; + currentwidth = width; + } + } + + if (itemsize > 0) + { + if (!empty) + { + line += ' '; + } + + line += *it; + empty = false; + } + } + + if (!empty) + { + output.push_back(line); + } + + return output; + } + + namespace detail + { + template + std::string Join(const T& array, const std::string &delimiter) + { + std::string res; + for (auto &element : array) + { + if (!res.empty()) + { + res += delimiter; + } + + res += element; + } + + return res; + } + } + + /** (INTERNAL) Wrap a string into a vector of lines + * + * This is quick and hacky, but works well enough. You can specify a + * different width for the first line + * + * \param width The width of the body + * \param firstlinewid the width of the first line, defaults to the width of the body + * \return the vector of lines + */ + inline std::vector Wrap(const std::string &in, const std::string::size_type width, std::string::size_type firstlinewidth = 0) + { + // Preserve existing line breaks + const auto newlineloc = in.find('\n'); + if (newlineloc != in.npos) + { + auto first = Wrap(std::string(in, 0, newlineloc), width); + auto second = Wrap(std::string(in, newlineloc + 1), width); + first.insert( + std::end(first), + std::make_move_iterator(std::begin(second)), + std::make_move_iterator(std::end(second))); + return first; + } + + std::istringstream stream(in); + std::string::size_type indent = 0; + + for (auto c : in) + { + if (!std::isspace(static_cast(c))) + { + break; + } + ++indent; + } + + return Wrap(std::istream_iterator(stream), std::istream_iterator(), + width, firstlinewidth, indent); + } + +#ifdef ARGS_NOEXCEPT + /// Error class, for when ARGS_NOEXCEPT is defined + enum class Error + { + None, + Usage, + Parse, + Validation, + Required, + Map, + Extra, + Help, + Subparser, + Completion, + }; +#else + /** Base error class + */ + class Error : public std::runtime_error + { + public: + Error(const std::string &problem) : std::runtime_error(problem) {} + virtual ~Error() {} + }; + + /** Errors that occur during usage + */ + class UsageError : public Error + { + public: + UsageError(const std::string &problem) : Error(problem) {} + virtual ~UsageError() {} + }; + + /** Errors that occur during regular parsing + */ + class ParseError : public Error + { + public: + ParseError(const std::string &problem) : Error(problem) {} + virtual ~ParseError() {} + }; + + /** Errors that are detected from group validation after parsing finishes + */ + class ValidationError : public Error + { + public: + ValidationError(const std::string &problem) : Error(problem) {} + virtual ~ValidationError() {} + }; + + /** Errors that when a required flag is omitted + */ + class RequiredError : public ValidationError + { + public: + RequiredError(const std::string &problem) : ValidationError(problem) {} + virtual ~RequiredError() {} + }; + + /** Errors in map lookups + */ + class MapError : public ParseError + { + public: + MapError(const std::string &problem) : ParseError(problem) {} + virtual ~MapError() {} + }; + + /** Error that occurs when a singular flag is specified multiple times + */ + class ExtraError : public ParseError + { + public: + ExtraError(const std::string &problem) : ParseError(problem) {} + virtual ~ExtraError() {} + }; + + /** An exception that indicates that the user has requested help + */ + class Help : public Error + { + public: + Help(const std::string &flag) : Error(flag) {} + virtual ~Help() {} + }; + + /** (INTERNAL) An exception that emulates coroutine-like control flow for subparsers. + */ + class SubparserError : public Error + { + public: + SubparserError() : Error("") {} + virtual ~SubparserError() {} + }; + + /** An exception that contains autocompletion reply + */ + class Completion : public Error + { + public: + Completion(const std::string &flag) : Error(flag) {} + virtual ~Completion() {} + }; +#endif + + /** A simple unified option type for unified initializer lists for the Matcher class. + */ + struct EitherFlag + { + const bool isShort; + const char shortFlag; + const std::string longFlag; + EitherFlag(const std::string &flag) : isShort(false), shortFlag(), longFlag(flag) {} + EitherFlag(const char *flag) : isShort(false), shortFlag(), longFlag(flag) {} + EitherFlag(const char flag) : isShort(true), shortFlag(flag), longFlag() {} + + /** Get just the long flags from an initializer list of EitherFlags + */ + static std::unordered_set GetLong(std::initializer_list flags) + { + std::unordered_set longFlags; + for (const EitherFlag &flag: flags) + { + if (!flag.isShort) + { + longFlags.insert(flag.longFlag); + } + } + return longFlags; + } + + /** Get just the short flags from an initializer list of EitherFlags + */ + static std::unordered_set GetShort(std::initializer_list flags) + { + std::unordered_set shortFlags; + for (const EitherFlag &flag: flags) + { + if (flag.isShort) + { + shortFlags.insert(flag.shortFlag); + } + } + return shortFlags; + } + + std::string str() const + { + return isShort ? std::string(1, shortFlag) : longFlag; + } + + std::string str(const std::string &shortPrefix, const std::string &longPrefix) const + { + return isShort ? shortPrefix + std::string(1, shortFlag) : longPrefix + longFlag; + } + }; + + + + /** A class of "matchers", specifying short and flags that can possibly be + * matched. + * + * This is supposed to be constructed and then passed in, not used directly + * from user code. + */ + class Matcher + { + private: + const std::unordered_set shortFlags; + const std::unordered_set longFlags; + + public: + /** Specify short and long flags separately as iterators + * + * ex: `args::Matcher(shortFlags.begin(), shortFlags.end(), longFlags.begin(), longFlags.end())` + */ + template + Matcher(ShortIt shortFlagsStart, ShortIt shortFlagsEnd, LongIt longFlagsStart, LongIt longFlagsEnd) : + shortFlags(shortFlagsStart, shortFlagsEnd), + longFlags(longFlagsStart, longFlagsEnd) + { + if (shortFlags.empty() && longFlags.empty()) + { +#ifndef ARGS_NOEXCEPT + throw UsageError("empty Matcher"); +#endif + } + } + +#ifdef ARGS_NOEXCEPT + /// Only for ARGS_NOEXCEPT + Error GetError() const noexcept + { + return shortFlags.empty() && longFlags.empty() ? Error::Usage : Error::None; + } +#endif + + /** Specify short and long flags separately as iterables + * + * ex: `args::Matcher(shortFlags, longFlags)` + */ + template + Matcher(Short &&shortIn, Long &&longIn) : + Matcher(std::begin(shortIn), std::end(shortIn), std::begin(longIn), std::end(longIn)) + {} + + /** Specify a mixed single initializer-list of both short and long flags + * + * This is the fancy one. It takes a single initializer list of + * any number of any mixed kinds of flags. Chars are + * automatically interpreted as short flags, and strings are + * automatically interpreted as long flags: + * + * args::Matcher{'a'} + * args::Matcher{"foo"} + * args::Matcher{'h', "help"} + * args::Matcher{"foo", 'f', 'F', "FoO"} + */ + Matcher(std::initializer_list in) : + Matcher(EitherFlag::GetShort(in), EitherFlag::GetLong(in)) {} + + Matcher(Matcher &&other) noexcept : shortFlags(std::move(other.shortFlags)), longFlags(std::move(other.longFlags)) + {} + + ~Matcher() {} + + /** (INTERNAL) Check if there is a match of a short flag + */ + bool Match(const char flag) const + { + return shortFlags.find(flag) != shortFlags.end(); + } + + /** (INTERNAL) Check if there is a match of a long flag + */ + bool Match(const std::string &flag) const + { + return longFlags.find(flag) != longFlags.end(); + } + + /** (INTERNAL) Check if there is a match of a flag + */ + bool Match(const EitherFlag &flag) const + { + return flag.isShort ? Match(flag.shortFlag) : Match(flag.longFlag); + } + + /** (INTERNAL) Get all flag strings as a vector, with the prefixes embedded + */ + std::vector GetFlagStrings() const + { + std::vector flagStrings; + flagStrings.reserve(shortFlags.size() + longFlags.size()); + for (const char flag: shortFlags) + { + flagStrings.emplace_back(flag); + } + for (const std::string &flag: longFlags) + { + flagStrings.emplace_back(flag); + } + return flagStrings; + } + + /** (INTERNAL) Get long flag if it exists or any short flag + */ + EitherFlag GetLongOrAny() const + { + if (!longFlags.empty()) + { + return *longFlags.begin(); + } + + if (!shortFlags.empty()) + { + return *shortFlags.begin(); + } + + // should be unreachable + return ' '; + } + + /** (INTERNAL) Get short flag if it exists or any long flag + */ + EitherFlag GetShortOrAny() const + { + if (!shortFlags.empty()) + { + return *shortFlags.begin(); + } + + if (!longFlags.empty()) + { + return *longFlags.begin(); + } + + // should be unreachable + return ' '; + } + }; + + /** Attributes for flags. + */ + enum class Options + { + /** Default options. + */ + None = 0x0, + + /** Flag can't be passed multiple times. + */ + Single = 0x01, + + /** Flag can't be omitted. + */ + Required = 0x02, + + /** Flag is excluded from usage line. + */ + HiddenFromUsage = 0x04, + + /** Flag is excluded from options help. + */ + HiddenFromDescription = 0x08, + + /** Flag is global and can be used in any subcommand. + */ + Global = 0x10, + + /** Flag stops a parser. + */ + KickOut = 0x20, + + /** Flag is excluded from auto completion. + */ + HiddenFromCompletion = 0x40, + + /** Flag is excluded from options help and usage line + */ + Hidden = HiddenFromUsage | HiddenFromDescription | HiddenFromCompletion, + }; + + inline Options operator | (Options lhs, Options rhs) + { + return static_cast(static_cast(lhs) | static_cast(rhs)); + } + + inline Options operator & (Options lhs, Options rhs) + { + return static_cast(static_cast(lhs) & static_cast(rhs)); + } + + class FlagBase; + class PositionalBase; + class Command; + class ArgumentParser; + + /** A simple structure of parameters for easy user-modifyable help menus + */ + struct HelpParams + { + /** The width of the help menu + */ + unsigned int width = 80; + /** The indent of the program line + */ + unsigned int progindent = 2; + /** The indent of the program trailing lines for long parameters + */ + unsigned int progtailindent = 4; + /** The indent of the description and epilogs + */ + unsigned int descriptionindent = 4; + /** The indent of the flags + */ + unsigned int flagindent = 6; + /** The indent of the flag descriptions + */ + unsigned int helpindent = 40; + /** The additional indent each group adds + */ + unsigned int eachgroupindent = 2; + + /** The minimum gutter between each flag and its help + */ + unsigned int gutter = 1; + + /** Show the terminator when both options and positional parameters are present + */ + bool showTerminator = true; + + /** Show the {OPTIONS} on the prog line when this is true + */ + bool showProglineOptions = true; + + /** Show the positionals on the prog line when this is true + */ + bool showProglinePositionals = true; + + /** The prefix for short flags + */ + std::string shortPrefix; + + /** The prefix for long flags + */ + std::string longPrefix; + + /** The separator for short flags + */ + std::string shortSeparator; + + /** The separator for long flags + */ + std::string longSeparator; + + /** The program name for help generation + */ + std::string programName; + + /** Show command's flags + */ + bool showCommandChildren = false; + + /** Show command's descriptions and epilog + */ + bool showCommandFullHelp = false; + + /** The postfix for progline when showProglineOptions is true and command has any flags + */ + std::string proglineOptions = "{OPTIONS}"; + + /** The prefix for progline when command has any subcommands + */ + std::string proglineCommand = "COMMAND"; + + /** The prefix for progline value + */ + std::string proglineValueOpen = " <"; + + /** The postfix for progline value + */ + std::string proglineValueClose = ">"; + + /** The prefix for progline required argument + */ + std::string proglineRequiredOpen = ""; + + /** The postfix for progline required argument + */ + std::string proglineRequiredClose = ""; + + /** The prefix for progline non-required argument + */ + std::string proglineNonrequiredOpen = "["; + + /** The postfix for progline non-required argument + */ + std::string proglineNonrequiredClose = "]"; + + /** Show flags in program line + */ + bool proglineShowFlags = false; + + /** Use short flags in program lines when possible + */ + bool proglinePreferShortFlags = false; + + /** Program line prefix + */ + std::string usageString; + + /** String shown in help before flags descriptions + */ + std::string optionsString = "OPTIONS:"; + + /** Display value name after all the long and short flags + */ + bool useValueNameOnce = false; + + /** Show value name + */ + bool showValueName = true; + + /** Add newline before flag description + */ + bool addNewlineBeforeDescription = false; + + /** The prefix for option value + */ + std::string valueOpen = "["; + + /** The postfix for option value + */ + std::string valueClose = "]"; + + /** Add choices to argument description + */ + bool addChoices = false; + + /** The prefix for choices + */ + std::string choiceString = "\nOne of: "; + + /** Add default values to argument description + */ + bool addDefault = false; + + /** The prefix for default values + */ + std::string defaultString = "\nDefault: "; + }; + + /** A number of arguments which can be consumed by an option. + * + * Represents a closed interval [min, max]. + */ + struct Nargs + { + const size_t min; + const size_t max; + + Nargs(size_t min_, size_t max_) : min{min_}, max{max_} + { +#ifndef ARGS_NOEXCEPT + if (max < min) + { + throw UsageError("Nargs: max > min"); + } +#endif + } + + Nargs(size_t num_) : min{num_}, max{num_} + { + } + + friend bool operator == (const Nargs &lhs, const Nargs &rhs) + { + return lhs.min == rhs.min && lhs.max == rhs.max; + } + + friend bool operator != (const Nargs &lhs, const Nargs &rhs) + { + return !(lhs == rhs); + } + }; + + /** Base class for all match types + */ + class Base + { + private: + Options options = {}; + + protected: + bool matched = false; + const std::string help; +#ifdef ARGS_NOEXCEPT + /// Only for ARGS_NOEXCEPT + mutable Error error = Error::None; + mutable std::string errorMsg; +#endif + + public: + Base(const std::string &help_, Options options_ = {}) : options(options_), help(help_) {} + virtual ~Base() {} + + Options GetOptions() const noexcept + { + return options; + } + + bool IsRequired() const noexcept + { + return (GetOptions() & Options::Required) != Options::None; + } + + virtual bool Matched() const noexcept + { + return matched; + } + + virtual void Validate(const std::string &, const std::string &) const + { + } + + operator bool() const noexcept + { + return Matched(); + } + + virtual std::vector> GetDescription(const HelpParams &, const unsigned indentLevel) const + { + std::tuple description; + std::get<1>(description) = help; + std::get<2>(description) = indentLevel; + return { std::move(description) }; + } + + virtual std::vector GetCommands() + { + return {}; + } + + virtual bool IsGroup() const + { + return false; + } + + virtual FlagBase *Match(const EitherFlag &) + { + return nullptr; + } + + virtual PositionalBase *GetNextPositional() + { + return nullptr; + } + + virtual std::vector GetAllFlags() + { + return {}; + } + + virtual bool HasFlag() const + { + return false; + } + + virtual bool HasPositional() const + { + return false; + } + + virtual bool HasCommand() const + { + return false; + } + + virtual std::vector GetProgramLine(const HelpParams &) const + { + return {}; + } + + /// Sets a kick-out value for building subparsers + void KickOut(bool kickout_) noexcept + { + if (kickout_) + { + options = options | Options::KickOut; + } + else + { + options = static_cast(static_cast(options) & ~static_cast(Options::KickOut)); + } + } + + /// Gets the kick-out value for building subparsers + bool KickOut() const noexcept + { + return (options & Options::KickOut) != Options::None; + } + + virtual void Reset() noexcept + { + matched = false; +#ifdef ARGS_NOEXCEPT + error = Error::None; + errorMsg.clear(); +#endif + } + +#ifdef ARGS_NOEXCEPT + /// Only for ARGS_NOEXCEPT + virtual Error GetError() const + { + return error; + } + + /// Only for ARGS_NOEXCEPT + std::string GetErrorMsg() const + { + return errorMsg; + } +#endif + }; + + /** Base class for all match types that have a name + */ + class NamedBase : public Base + { + protected: + const std::string name; + bool kickout = false; + std::string defaultString; + bool defaultStringManual = false; + std::vector choicesStrings; + bool choicesStringManual = false; + + virtual std::string GetDefaultString(const HelpParams&) const { return {}; } + + virtual std::vector GetChoicesStrings(const HelpParams&) const { return {}; } + + virtual std::string GetNameString(const HelpParams&) const { return Name(); } + + void AddDescriptionPostfix(std::string &dest, const bool isManual, const std::string &manual, bool isGenerated, const std::string &generated, const std::string &str) const + { + if (isManual && !manual.empty()) + { + dest += str; + dest += manual; + } + else if (!isManual && isGenerated && !generated.empty()) + { + dest += str; + dest += generated; + } + } + + public: + NamedBase(const std::string &name_, const std::string &help_, Options options_ = {}) : Base(help_, options_), name(name_) {} + virtual ~NamedBase() {} + + /** Sets default value string that will be added to argument description. + * Use empty string to disable it for this argument. + */ + void HelpDefault(const std::string &str) + { + defaultStringManual = true; + defaultString = str; + } + + /** Gets default value string that will be added to argument description. + */ + std::string HelpDefault(const HelpParams ¶ms) const + { + return defaultStringManual ? defaultString : GetDefaultString(params); + } + + /** Sets choices strings that will be added to argument description. + * Use empty vector to disable it for this argument. + */ + void HelpChoices(const std::vector &array) + { + choicesStringManual = true; + choicesStrings = array; + } + + /** Gets choices strings that will be added to argument description. + */ + std::vector HelpChoices(const HelpParams ¶ms) const + { + return choicesStringManual ? choicesStrings : GetChoicesStrings(params); + } + + virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned indentLevel) const override + { + std::tuple description; + std::get<0>(description) = GetNameString(params); + std::get<1>(description) = help; + std::get<2>(description) = indentLevel; + + AddDescriptionPostfix(std::get<1>(description), choicesStringManual, detail::Join(choicesStrings, ", "), params.addChoices, detail::Join(GetChoicesStrings(params), ", "), params.choiceString); + AddDescriptionPostfix(std::get<1>(description), defaultStringManual, defaultString, params.addDefault, GetDefaultString(params), params.defaultString); + + return { std::move(description) }; + } + + virtual std::string Name() const + { + return name; + } + }; + + namespace detail + { + template + using vector = std::vector>; + + template + using unordered_map = std::unordered_map, + std::equal_to, std::allocator > >; + + template + class is_streamable + { + template + static auto test(int) + -> decltype( std::declval() << std::declval(), std::true_type() ); + + template + static auto test(...) -> std::false_type; + + public: + using type = decltype(test(0)); + }; + + template + using IsConvertableToString = typename is_streamable::type; + + template + typename std::enable_if::value, std::string>::type + ToString(const T &value) + { + std::ostringstream s; + s << value; + return s.str(); + } + + template + typename std::enable_if::value, std::string>::type + ToString(const T &) + { + return {}; + } + + template + std::vector MapKeysToStrings(const T &map) + { + std::vector res; + using K = typename std::decayfirst)>::type; + if (IsConvertableToString::value) + { + for (const auto &p : map) + { + res.push_back(detail::ToString(p.first)); + } + + std::sort(res.begin(), res.end()); + } + return res; + } + } + + /** Base class for all flag options + */ + class FlagBase : public NamedBase + { + protected: + const Matcher matcher; + + virtual std::string GetNameString(const HelpParams ¶ms) const override + { + const std::string postfix = !params.showValueName || NumberOfArguments() == 0 ? std::string() : Name(); + std::string flags; + const auto flagStrings = matcher.GetFlagStrings(); + const bool useValueNameOnce = flagStrings.size() == 1 ? false : params.useValueNameOnce; + for (auto it = flagStrings.begin(); it != flagStrings.end(); ++it) + { + auto &flag = *it; + if (it != flagStrings.begin()) + { + flags += ", "; + } + + flags += flag.isShort ? params.shortPrefix : params.longPrefix; + flags += flag.str(); + + if (!postfix.empty() && (!useValueNameOnce || it + 1 == flagStrings.end())) + { + flags += flag.isShort ? params.shortSeparator : params.longSeparator; + flags += params.valueOpen + postfix + params.valueClose; + } + } + + return flags; + } + + public: + FlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false) : NamedBase(name_, help_, extraError_ ? Options::Single : Options()), matcher(std::move(matcher_)) {} + + FlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) : NamedBase(name_, help_, options_), matcher(std::move(matcher_)) {} + + virtual ~FlagBase() {} + + virtual FlagBase *Match(const EitherFlag &flag) override + { + if (matcher.Match(flag)) + { + if ((GetOptions() & Options::Single) != Options::None && matched) + { + std::ostringstream problem; + problem << "Flag '" << flag.str() << "' was passed multiple times, but is only allowed to be passed once"; +#ifdef ARGS_NOEXCEPT + error = Error::Extra; + errorMsg = problem.str(); +#else + throw ExtraError(problem.str()); +#endif + } + matched = true; + return this; + } + return nullptr; + } + + virtual std::vector GetAllFlags() override + { + return { this }; + } + + const Matcher &GetMatcher() const + { + return matcher; + } + + virtual void Validate(const std::string &shortPrefix, const std::string &longPrefix) const override + { + if (!Matched() && IsRequired()) + { + std::ostringstream problem; + problem << "Flag '" << matcher.GetLongOrAny().str(shortPrefix, longPrefix) << "' is required"; +#ifdef ARGS_NOEXCEPT + error = Error::Required; + errorMsg = problem.str(); +#else + throw RequiredError(problem.str()); +#endif + } + } + + virtual std::vector GetProgramLine(const HelpParams ¶ms) const override + { + if (!params.proglineShowFlags) + { + return {}; + } + + const std::string postfix = NumberOfArguments() == 0 ? std::string() : Name(); + const EitherFlag flag = params.proglinePreferShortFlags ? matcher.GetShortOrAny() : matcher.GetLongOrAny(); + std::string res = flag.str(params.shortPrefix, params.longPrefix); + if (!postfix.empty()) + { + res += params.proglineValueOpen + postfix + params.proglineValueClose; + } + + return { IsRequired() ? params.proglineRequiredOpen + res + params.proglineRequiredClose + : params.proglineNonrequiredOpen + res + params.proglineNonrequiredClose }; + } + + virtual bool HasFlag() const override + { + return true; + } + +#ifdef ARGS_NOEXCEPT + /// Only for ARGS_NOEXCEPT + virtual Error GetError() const override + { + const auto nargs = NumberOfArguments(); + if (nargs.min > nargs.max) + { + return Error::Usage; + } + + const auto matcherError = matcher.GetError(); + if (matcherError != Error::None) + { + return matcherError; + } + + return error; + } +#endif + + /** Defines how many values can be consumed by this option. + * + * \return closed interval [min, max] + */ + virtual Nargs NumberOfArguments() const noexcept = 0; + + /** Parse values of this option. + * + * \param value Vector of values. It's size must be in NumberOfArguments() interval. + */ + virtual void ParseValue(const std::vector &value) = 0; + }; + + /** Base class for value-accepting flag options + */ + class ValueFlagBase : public FlagBase + { + public: + ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false) : FlagBase(name_, help_, std::move(matcher_), extraError_) {} + ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) : FlagBase(name_, help_, std::move(matcher_), options_) {} + virtual ~ValueFlagBase() {} + + virtual Nargs NumberOfArguments() const noexcept override + { + return 1; + } + }; + + class CompletionFlag : public ValueFlagBase + { + public: + std::vector reply; + size_t cword = 0; + std::string syntax; + + template + CompletionFlag(GroupClass &group_, Matcher &&matcher_): ValueFlagBase("completion", "completion flag", std::move(matcher_), Options::Hidden) + { + group_.AddCompletion(*this); + } + + virtual ~CompletionFlag() {} + + virtual Nargs NumberOfArguments() const noexcept override + { + return 2; + } + + virtual void ParseValue(const std::vector &value_) override + { + syntax = value_.at(0); + std::istringstream(value_.at(1)) >> cword; + } + + /** Get the completion reply + */ + std::string Get() noexcept + { + return detail::Join(reply, "\n"); + } + + virtual void Reset() noexcept override + { + ValueFlagBase::Reset(); + cword = 0; + syntax.clear(); + reply.clear(); + } + }; + + + /** Base class for positional options + */ + class PositionalBase : public NamedBase + { + protected: + bool ready; + + public: + PositionalBase(const std::string &name_, const std::string &help_, Options options_ = {}) : NamedBase(name_, help_, options_), ready(true) {} + virtual ~PositionalBase() {} + + bool Ready() + { + return ready; + } + + virtual void ParseValue(const std::string &value_) = 0; + + virtual void Reset() noexcept override + { + matched = false; + ready = true; +#ifdef ARGS_NOEXCEPT + error = Error::None; + errorMsg.clear(); +#endif + } + + virtual PositionalBase *GetNextPositional() override + { + return Ready() ? this : nullptr; + } + + virtual bool HasPositional() const override + { + return true; + } + + virtual std::vector GetProgramLine(const HelpParams ¶ms) const override + { + return { IsRequired() ? params.proglineRequiredOpen + Name() + params.proglineRequiredClose + : params.proglineNonrequiredOpen + Name() + params.proglineNonrequiredClose }; + } + + virtual void Validate(const std::string &, const std::string &) const override + { + if (IsRequired() && !Matched()) + { + std::ostringstream problem; + problem << "Option '" << Name() << "' is required"; +#ifdef ARGS_NOEXCEPT + error = Error::Required; + errorMsg = problem.str(); +#else + throw RequiredError(problem.str()); +#endif + } + } + }; + + /** Class for all kinds of validating groups, including ArgumentParser + */ + class Group : public Base + { + private: + std::vector children; + std::function validator; + + public: + /** Default validators + */ + struct Validators + { + static bool Xor(const Group &group) + { + return group.MatchedChildren() == 1; + } + + static bool AtLeastOne(const Group &group) + { + return group.MatchedChildren() >= 1; + } + + static bool AtMostOne(const Group &group) + { + return group.MatchedChildren() <= 1; + } + + static bool All(const Group &group) + { + return group.Children().size() == group.MatchedChildren(); + } + + static bool AllOrNone(const Group &group) + { + return (All(group) || None(group)); + } + + static bool AllChildGroups(const Group &group) + { + return std::none_of(std::begin(group.Children()), std::end(group.Children()), [](const Base* child) -> bool { + return child->IsGroup() && !child->Matched(); + }); + } + + static bool DontCare(const Group &) + { + return true; + } + + static bool CareTooMuch(const Group &) + { + return false; + } + + static bool None(const Group &group) + { + return group.MatchedChildren() == 0; + } + }; + /// If help is empty, this group will not be printed in help output + Group(const std::string &help_ = std::string(), const std::function &validator_ = Validators::DontCare, Options options_ = {}) : Base(help_, options_), validator(validator_) {} + /// If help is empty, this group will not be printed in help output + Group(Group &group_, const std::string &help_ = std::string(), const std::function &validator_ = Validators::DontCare, Options options_ = {}) : Base(help_, options_), validator(validator_) + { + group_.Add(*this); + } + virtual ~Group() {} + + /** Append a child to this Group. + */ + void Add(Base &child) + { + children.emplace_back(&child); + } + + /** Get all this group's children + */ + const std::vector &Children() const + { + return children; + } + + /** Return the first FlagBase that matches flag, or nullptr + * + * \param flag The flag with prefixes stripped + * \return the first matching FlagBase pointer, or nullptr if there is no match + */ + virtual FlagBase *Match(const EitherFlag &flag) override + { + for (Base *child: Children()) + { + if (FlagBase *match = child->Match(flag)) + { + return match; + } + } + return nullptr; + } + + virtual std::vector GetAllFlags() override + { + std::vector res; + for (Base *child: Children()) + { + auto childRes = child->GetAllFlags(); + res.insert(res.end(), childRes.begin(), childRes.end()); + } + return res; + } + + virtual void Validate(const std::string &shortPrefix, const std::string &longPrefix) const override + { + for (Base *child: Children()) + { + child->Validate(shortPrefix, longPrefix); + } + } + + /** Get the next ready positional, or nullptr if there is none + * + * \return the first ready PositionalBase pointer, or nullptr if there is no match + */ + virtual PositionalBase *GetNextPositional() override + { + for (Base *child: Children()) + { + if (auto next = child->GetNextPositional()) + { + return next; + } + } + return nullptr; + } + + /** Get whether this has any FlagBase children + * + * \return Whether or not there are any FlagBase children + */ + virtual bool HasFlag() const override + { + return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasFlag(); }); + } + + /** Get whether this has any PositionalBase children + * + * \return Whether or not there are any PositionalBase children + */ + virtual bool HasPositional() const override + { + return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasPositional(); }); + } + + /** Get whether this has any Command children + * + * \return Whether or not there are any Command children + */ + virtual bool HasCommand() const override + { + return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasCommand(); }); + } + + /** Count the number of matched children this group has + */ + std::vector::size_type MatchedChildren() const + { + // Cast to avoid warnings from -Wsign-conversion + return static_cast::size_type>( + std::count_if(std::begin(Children()), std::end(Children()), [](const Base *child){return child->Matched();})); + } + + /** Whether or not this group matches validation + */ + virtual bool Matched() const noexcept override + { + return validator(*this); + } + + /** Get validation + */ + bool Get() const + { + return Matched(); + } + + /** Get all the child descriptions for help generation + */ + virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned int indent) const override + { + std::vector> descriptions; + + // Push that group description on the back if not empty + unsigned addindent = 0; + if (!help.empty()) + { + descriptions.emplace_back(help, "", indent); + addindent = 1; + } + + for (Base *child: Children()) + { + if ((child->GetOptions() & Options::HiddenFromDescription) != Options::None) + { + continue; + } + + auto groupDescriptions = child->GetDescription(params, indent + addindent); + descriptions.insert( + std::end(descriptions), + std::make_move_iterator(std::begin(groupDescriptions)), + std::make_move_iterator(std::end(groupDescriptions))); + } + return descriptions; + } + + /** Get the names of positional parameters + */ + virtual std::vector GetProgramLine(const HelpParams ¶ms) const override + { + std::vector names; + for (Base *child: Children()) + { + if ((child->GetOptions() & Options::HiddenFromUsage) != Options::None) + { + continue; + } + + auto groupNames = child->GetProgramLine(params); + names.insert( + std::end(names), + std::make_move_iterator(std::begin(groupNames)), + std::make_move_iterator(std::end(groupNames))); + } + return names; + } + + virtual std::vector GetCommands() override + { + std::vector res; + for (const auto &child : Children()) + { + auto subparsers = child->GetCommands(); + res.insert(std::end(res), std::begin(subparsers), std::end(subparsers)); + } + return res; + } + + virtual bool IsGroup() const override + { + return true; + } + + virtual void Reset() noexcept override + { + Base::Reset(); + + for (auto &child: Children()) + { + child->Reset(); + } +#ifdef ARGS_NOEXCEPT + error = Error::None; + errorMsg.clear(); +#endif + } + +#ifdef ARGS_NOEXCEPT + /// Only for ARGS_NOEXCEPT + virtual Error GetError() const override + { + if (error != Error::None) + { + return error; + } + + auto it = std::find_if(Children().begin(), Children().end(), [](const Base *child){return child->GetError() != Error::None;}); + if (it == Children().end()) + { + return Error::None; + } else + { + return (*it)->GetError(); + } + } +#endif + + }; + + /** Class for using global options in ArgumentParser. + */ + class GlobalOptions : public Group + { + public: + GlobalOptions(Group &base, Base &options_) : Group(base, {}, Group::Validators::DontCare, Options::Global) + { + Add(options_); + } + }; + + /** Utility class for building subparsers with coroutines/callbacks. + * + * Brief example: + * \code + * Command command(argumentParser, "command", "my command", [](args::Subparser &s) + * { + * // your command flags/positionals + * s.Parse(); //required + * //your command code + * }); + * \endcode + * + * For ARGS_NOEXCEPT mode don't forget to check `s.GetError()` after `s.Parse()` + * and return if it isn't equals to args::Error::None. + * + * \sa Command + */ + class Subparser : public Group + { + private: + std::vector args; + std::vector kicked; + ArgumentParser *parser = nullptr; + const HelpParams &helpParams; + const Command &command; + bool isParsed = false; + + public: + Subparser(std::vector args_, ArgumentParser &parser_, const Command &command_, const HelpParams &helpParams_) + : Group({}, Validators::AllChildGroups), args(std::move(args_)), parser(&parser_), helpParams(helpParams_), command(command_) + { + } + + Subparser(const Command &command_, const HelpParams &helpParams_) : Group({}, Validators::AllChildGroups), helpParams(helpParams_), command(command_) + { + } + + Subparser(const Subparser&) = delete; + Subparser(Subparser&&) = delete; + Subparser &operator = (const Subparser&) = delete; + Subparser &operator = (Subparser&&) = delete; + + const Command &GetCommand() + { + return command; + } + + /** (INTERNAL) Determines whether Parse was called or not. + */ + bool IsParsed() const + { + return isParsed; + } + + /** Continue parsing arguments for new command. + */ + void Parse(); + + /** Returns a vector of kicked out arguments. + * + * \sa Base::KickOut + */ + const std::vector &KickedOut() const noexcept + { + return kicked; + } + }; + + /** Main class for building subparsers. + * + * /sa Subparser + */ + class Command : public Group + { + private: + friend class Subparser; + + std::string name; + std::string help; + std::string description; + std::string epilog; + std::string proglinePostfix; + + std::function parserCoroutine; + bool commandIsRequired = true; + Command *selectedCommand = nullptr; + + mutable std::vector> subparserDescription; + mutable std::vector subparserProgramLine; + mutable bool subparserHasFlag = false; + mutable bool subparserHasPositional = false; + mutable bool subparserHasCommand = false; +#ifdef ARGS_NOEXCEPT + mutable Error subparserError = Error::None; +#endif + mutable Subparser *subparser = nullptr; + + protected: + + class RaiiSubparser + { + public: + RaiiSubparser(ArgumentParser &parser_, std::vector args_); + RaiiSubparser(const Command &command_, const HelpParams ¶ms_); + + ~RaiiSubparser() + { + command.subparser = oldSubparser; + } + + Subparser &Parser() + { + return parser; + } + + private: + const Command &command; + Subparser parser; + Subparser *oldSubparser; + }; + + Command() = default; + + std::function &GetCoroutine() + { + return selectedCommand != nullptr ? selectedCommand->GetCoroutine() : parserCoroutine; + } + + Command &SelectedCommand() + { + Command *res = this; + while (res->selectedCommand != nullptr) + { + res = res->selectedCommand; + } + + return *res; + } + + const Command &SelectedCommand() const + { + const Command *res = this; + while (res->selectedCommand != nullptr) + { + res = res->selectedCommand; + } + + return *res; + } + + void UpdateSubparserHelp(const HelpParams ¶ms) const + { + if (parserCoroutine) + { + RaiiSubparser coro(*this, params); +#ifndef ARGS_NOEXCEPT + try + { + parserCoroutine(coro.Parser()); + } + catch (args::SubparserError&) + { + } +#else + parserCoroutine(coro.Parser()); +#endif + } + } + + public: + Command(Group &base_, std::string name_, std::string help_, std::function coroutine_ = {}) + : name(std::move(name_)), help(std::move(help_)), parserCoroutine(std::move(coroutine_)) + { + base_.Add(*this); + } + + /** The description that appears on the prog line after options + */ + const std::string &ProglinePostfix() const + { return proglinePostfix; } + + /** The description that appears on the prog line after options + */ + void ProglinePostfix(const std::string &proglinePostfix_) + { this->proglinePostfix = proglinePostfix_; } + + /** The description that appears above options + */ + const std::string &Description() const + { return description; } + /** The description that appears above options + */ + + void Description(const std::string &description_) + { this->description = description_; } + + /** The description that appears below options + */ + const std::string &Epilog() const + { return epilog; } + + /** The description that appears below options + */ + void Epilog(const std::string &epilog_) + { this->epilog = epilog_; } + + /** The name of command + */ + const std::string &Name() const + { return name; } + + /** The description of command + */ + const std::string &Help() const + { return help; } + + /** If value is true, parser will fail if no command was parsed. + * + * Default: true. + */ + void RequireCommand(bool value) + { commandIsRequired = value; } + + virtual bool IsGroup() const override + { return false; } + + virtual bool Matched() const noexcept override + { return Base::Matched(); } + + operator bool() const noexcept + { return Matched(); } + + void Match() noexcept + { matched = true; } + + void SelectCommand(Command *c) noexcept + { + selectedCommand = c; + + if (c != nullptr) + { + c->Match(); + } + } + + virtual FlagBase *Match(const EitherFlag &flag) override + { + if (selectedCommand != nullptr) + { + if (auto *res = selectedCommand->Match(flag)) + { + return res; + } + + for (auto *child: Children()) + { + if ((child->GetOptions() & Options::Global) != Options::None) + { + if (auto *res = child->Match(flag)) + { + return res; + } + } + } + + return nullptr; + } + + if (subparser != nullptr) + { + return subparser->Match(flag); + } + + return Matched() ? Group::Match(flag) : nullptr; + } + + virtual std::vector GetAllFlags() override + { + std::vector res; + + if (!Matched()) + { + return res; + } + + for (auto *child: Children()) + { + if (selectedCommand == nullptr || (child->GetOptions() & Options::Global) != Options::None) + { + auto childFlags = child->GetAllFlags(); + res.insert(res.end(), childFlags.begin(), childFlags.end()); + } + } + + if (selectedCommand != nullptr) + { + auto childFlags = selectedCommand->GetAllFlags(); + res.insert(res.end(), childFlags.begin(), childFlags.end()); + } + + if (subparser != nullptr) + { + auto childFlags = subparser->GetAllFlags(); + res.insert(res.end(), childFlags.begin(), childFlags.end()); + } + + return res; + } + + virtual PositionalBase *GetNextPositional() override + { + if (selectedCommand != nullptr) + { + if (auto *res = selectedCommand->GetNextPositional()) + { + return res; + } + + for (auto *child: Children()) + { + if ((child->GetOptions() & Options::Global) != Options::None) + { + if (auto *res = child->GetNextPositional()) + { + return res; + } + } + } + + return nullptr; + } + + if (subparser != nullptr) + { + return subparser->GetNextPositional(); + } + + return Matched() ? Group::GetNextPositional() : nullptr; + } + + virtual bool HasFlag() const override + { + return subparserHasFlag || Group::HasFlag(); + } + + virtual bool HasPositional() const override + { + return subparserHasPositional || Group::HasPositional(); + } + + virtual bool HasCommand() const override + { + return true; + } + + std::vector GetCommandProgramLine(const HelpParams ¶ms) const + { + UpdateSubparserHelp(params); + + auto res = Group::GetProgramLine(params); + res.insert(res.end(), subparserProgramLine.begin(), subparserProgramLine.end()); + + if (!params.proglineCommand.empty() && (Group::HasCommand() || subparserHasCommand)) + { + res.insert(res.begin(), commandIsRequired ? params.proglineCommand : "[" + params.proglineCommand + "]"); + } + + if (!Name().empty()) + { + res.insert(res.begin(), Name()); + } + + if ((subparserHasFlag || Group::HasFlag()) && params.showProglineOptions && !params.proglineShowFlags) + { + res.push_back(params.proglineOptions); + } + + if (!ProglinePostfix().empty()) + { + std::string line; + for (auto c : ProglinePostfix()) + { + if (std::isspace(static_cast(c))) + { + if (!line.empty()) + { + res.push_back(line); + line.clear(); + } + + if (c == '\n') + { + res.push_back("\n"); + } + } + else + { + line += c; + } + } + + if (!line.empty()) + { + res.push_back(line); + } + } + + return res; + } + + virtual std::vector GetProgramLine(const HelpParams ¶ms) const override + { + if (!Matched()) + { + return {}; + } + + return GetCommandProgramLine(params); + } + + virtual std::vector GetCommands() override + { + if (selectedCommand != nullptr) + { + return selectedCommand->GetCommands(); + } + + if (Matched()) + { + return Group::GetCommands(); + } + + return { this }; + } + + virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned int indent) const override + { + std::vector> descriptions; + unsigned addindent = 0; + + UpdateSubparserHelp(params); + + if (!Matched()) + { + if (params.showCommandFullHelp) + { + std::ostringstream s; + bool empty = true; + for (const auto &progline: GetCommandProgramLine(params)) + { + if (!empty) + { + s << ' '; + } + else + { + empty = false; + } + + s << progline; + } + + descriptions.emplace_back(s.str(), "", indent); + } + else + { + descriptions.emplace_back(Name(), help, indent); + } + + if (!params.showCommandChildren && !params.showCommandFullHelp) + { + return descriptions; + } + + addindent = 1; + } + + if (params.showCommandFullHelp && !Matched()) + { + descriptions.emplace_back("", "", indent + addindent); + descriptions.emplace_back(Description().empty() ? Help() : Description(), "", indent + addindent); + descriptions.emplace_back("", "", indent + addindent); + } + + for (Base *child: Children()) + { + if ((child->GetOptions() & Options::HiddenFromDescription) != Options::None) + { + continue; + } + + auto groupDescriptions = child->GetDescription(params, indent + addindent); + descriptions.insert( + std::end(descriptions), + std::make_move_iterator(std::begin(groupDescriptions)), + std::make_move_iterator(std::end(groupDescriptions))); + } + + for (auto childDescription: subparserDescription) + { + std::get<2>(childDescription) += indent + addindent; + descriptions.push_back(std::move(childDescription)); + } + + if (params.showCommandFullHelp && !Matched()) + { + descriptions.emplace_back("", "", indent + addindent); + if (!Epilog().empty()) + { + descriptions.emplace_back(Epilog(), "", indent + addindent); + descriptions.emplace_back("", "", indent + addindent); + } + } + + return descriptions; + } + + virtual void Validate(const std::string &shortprefix, const std::string &longprefix) const override + { + if (!Matched()) + { + return; + } + + auto onValidationError = [&] + { + std::ostringstream problem; + problem << "Group validation failed somewhere!"; +#ifdef ARGS_NOEXCEPT + error = Error::Validation; + errorMsg = problem.str(); +#else + throw ValidationError(problem.str()); +#endif + }; + + for (Base *child: Children()) + { + if (child->IsGroup() && !child->Matched()) + { + onValidationError(); + } + + child->Validate(shortprefix, longprefix); + } + + if (subparser != nullptr) + { + subparser->Validate(shortprefix, longprefix); + if (!subparser->Matched()) + { + onValidationError(); + } + } + + if (selectedCommand == nullptr && commandIsRequired && (Group::HasCommand() || subparserHasCommand)) + { + std::ostringstream problem; + problem << "Command is required"; +#ifdef ARGS_NOEXCEPT + error = Error::Validation; + errorMsg = problem.str(); +#else + throw ValidationError(problem.str()); +#endif + } + } + + virtual void Reset() noexcept override + { + Group::Reset(); + selectedCommand = nullptr; + subparserProgramLine.clear(); + subparserDescription.clear(); + subparserHasFlag = false; + subparserHasPositional = false; + subparserHasCommand = false; +#ifdef ARGS_NOEXCEPT + subparserError = Error::None; +#endif + } + +#ifdef ARGS_NOEXCEPT + /// Only for ARGS_NOEXCEPT + virtual Error GetError() const override + { + if (!Matched()) + { + return Error::None; + } + + if (error != Error::None) + { + return error; + } + + if (subparserError != Error::None) + { + return subparserError; + } + + return Group::GetError(); + } +#endif + }; + + /** The main user facing command line argument parser class + */ + class ArgumentParser : public Command + { + friend class Subparser; + + private: + std::string longprefix; + std::string shortprefix; + + std::string longseparator; + + std::string terminator; + + bool allowJoinedShortValue = true; + bool allowJoinedLongValue = true; + bool allowSeparateShortValue = true; + bool allowSeparateLongValue = true; + + CompletionFlag *completion = nullptr; + bool readCompletion = false; + + protected: + enum class OptionType + { + LongFlag, + ShortFlag, + Positional + }; + + OptionType ParseOption(const std::string &s, bool allowEmpty = false) + { + if (s.find(longprefix) == 0 && (allowEmpty || s.length() > longprefix.length())) + { + return OptionType::LongFlag; + } + + if (s.find(shortprefix) == 0 && (allowEmpty || s.length() > shortprefix.length())) + { + return OptionType::ShortFlag; + } + + return OptionType::Positional; + } + + template + bool Complete(FlagBase &flag, It it, It end) + { + auto nextIt = it; + if (!readCompletion || (++nextIt != end)) + { + return false; + } + + const auto &chunk = *it; + for (auto &choice : flag.HelpChoices(helpParams)) + { + AddCompletionReply(chunk, choice); + } + +#ifndef ARGS_NOEXCEPT + throw Completion(completion->Get()); +#else + return true; +#endif + } + + /** (INTERNAL) Parse flag's values + * + * \param arg The string to display in error message as a flag name + * \param[in, out] it The iterator to first value. It will point to the last value + * \param end The end iterator + * \param joinedArg Joined value (e.g. bar in --foo=bar) + * \param canDiscardJoined If true joined value can be parsed as flag not as a value (as in -abcd) + * \param[out] values The vector to store parsed arg's values + */ + template + std::string ParseArgsValues(FlagBase &flag, const std::string &arg, It &it, It end, + const bool allowSeparate, const bool allowJoined, + const bool hasJoined, const std::string &joinedArg, + const bool canDiscardJoined, std::vector &values) + { + values.clear(); + + Nargs nargs = flag.NumberOfArguments(); + + if (hasJoined && !allowJoined && nargs.min != 0) + { + return "Flag '" + arg + "' was passed a joined argument, but these are disallowed"; + } + + if (hasJoined) + { + if (!canDiscardJoined || nargs.max != 0) + { + values.push_back(joinedArg); + } + } else if (!allowSeparate) + { + if (nargs.min != 0) + { + return "Flag '" + arg + "' was passed a separate argument, but these are disallowed"; + } + } else + { + auto valueIt = it; + ++valueIt; + + while (valueIt != end && + values.size() < nargs.max && + (values.size() < nargs.min || ParseOption(*valueIt) == OptionType::Positional)) + { + if (Complete(flag, valueIt, end)) + { + it = end; + return ""; + } + + values.push_back(*valueIt); + ++it; + ++valueIt; + } + } + + if (values.size() > nargs.max) + { + return "Passed an argument into a non-argument flag: " + arg; + } else if (values.size() < nargs.min) + { + if (nargs.min == 1 && nargs.max == 1) + { + return "Flag '" + arg + "' requires an argument but received none"; + } else if (nargs.min == 1) + { + return "Flag '" + arg + "' requires at least one argument but received none"; + } else if (nargs.min != nargs.max) + { + return "Flag '" + arg + "' requires at least " + std::to_string(nargs.min) + + " arguments but received " + std::to_string(values.size()); + } else + { + return "Flag '" + arg + "' requires " + std::to_string(nargs.min) + + " arguments but received " + std::to_string(values.size()); + } + } + + return {}; + } + + template + bool ParseLong(It &it, It end) + { + const auto &chunk = *it; + const auto argchunk = chunk.substr(longprefix.size()); + // Try to separate it, in case of a separator: + const auto separator = longseparator.empty() ? argchunk.npos : argchunk.find(longseparator); + // If the separator is in the argument, separate it. + const auto arg = (separator != argchunk.npos ? + std::string(argchunk, 0, separator) + : argchunk); + const auto joined = (separator != argchunk.npos ? + argchunk.substr(separator + longseparator.size()) + : std::string()); + + if (auto flag = Match(arg)) + { + std::vector values; + const std::string errorMessage = ParseArgsValues(*flag, arg, it, end, allowSeparateLongValue, allowJoinedLongValue, + separator != argchunk.npos, joined, false, values); + if (!errorMessage.empty()) + { +#ifndef ARGS_NOEXCEPT + throw ParseError(errorMessage); +#else + error = Error::Parse; + errorMsg = errorMessage; + return false; +#endif + } + + if (!readCompletion) + { + flag->ParseValue(values); + } + + if (flag->KickOut()) + { + ++it; + return false; + } + } else + { + const std::string errorMessage("Flag could not be matched: " + arg); +#ifndef ARGS_NOEXCEPT + throw ParseError(errorMessage); +#else + error = Error::Parse; + errorMsg = errorMessage; + return false; +#endif + } + + return true; + } + + template + bool ParseShort(It &it, It end) + { + const auto &chunk = *it; + const auto argchunk = chunk.substr(shortprefix.size()); + for (auto argit = std::begin(argchunk); argit != std::end(argchunk); ++argit) + { + const auto arg = *argit; + + if (auto flag = Match(arg)) + { + const std::string value(argit + 1, std::end(argchunk)); + std::vector values; + const std::string errorMessage = ParseArgsValues(*flag, std::string(1, arg), it, end, + allowSeparateShortValue, allowJoinedShortValue, + !value.empty(), value, !value.empty(), values); + + if (!errorMessage.empty()) + { +#ifndef ARGS_NOEXCEPT + throw ParseError(errorMessage); +#else + error = Error::Parse; + errorMsg = errorMessage; + return false; +#endif + } + + if (!readCompletion) + { + flag->ParseValue(values); + } + + if (flag->KickOut()) + { + ++it; + return false; + } + + if (!values.empty()) + { + break; + } + } else + { + const std::string errorMessage("Flag could not be matched: '" + std::string(1, arg) + "'"); +#ifndef ARGS_NOEXCEPT + throw ParseError(errorMessage); +#else + error = Error::Parse; + errorMsg = errorMessage; + return false; +#endif + } + } + + return true; + } + + bool AddCompletionReply(const std::string &cur, const std::string &choice) + { + if (cur.empty() || choice.find(cur) == 0) + { + if (completion->syntax == "bash" && ParseOption(choice) == OptionType::LongFlag && choice.find(longseparator) != std::string::npos) + { + completion->reply.push_back(choice.substr(choice.find(longseparator) + 1)); + } else + { + completion->reply.push_back(choice); + } + return true; + } + + return false; + } + + template + bool Complete(It it, It end) + { + auto nextIt = it; + if (!readCompletion || (++nextIt != end)) + { + return false; + } + + const auto &chunk = *it; + auto pos = GetNextPositional(); + std::vector commands = GetCommands(); + const auto optionType = ParseOption(chunk, true); + + if (!commands.empty() && (chunk.empty() || optionType == OptionType::Positional)) + { + for (auto &cmd : commands) + { + if ((cmd->GetOptions() & Options::HiddenFromCompletion) == Options::None) + { + AddCompletionReply(chunk, cmd->Name()); + } + } + } else + { + bool hasPositionalCompletion = true; + + if (!commands.empty()) + { + for (auto &cmd : commands) + { + if ((cmd->GetOptions() & Options::HiddenFromCompletion) == Options::None) + { + AddCompletionReply(chunk, cmd->Name()); + } + } + } else if (pos) + { + if ((pos->GetOptions() & Options::HiddenFromCompletion) == Options::None) + { + auto choices = pos->HelpChoices(helpParams); + hasPositionalCompletion = !choices.empty() || optionType != OptionType::Positional; + for (auto &choice : choices) + { + AddCompletionReply(chunk, choice); + } + } + } + + if (hasPositionalCompletion) + { + auto flags = GetAllFlags(); + for (auto flag : flags) + { + if ((flag->GetOptions() & Options::HiddenFromCompletion) != Options::None) + { + continue; + } + + auto &matcher = flag->GetMatcher(); + if (!AddCompletionReply(chunk, matcher.GetShortOrAny().str(shortprefix, longprefix))) + { + for (auto &flagName : matcher.GetFlagStrings()) + { + if (AddCompletionReply(chunk, flagName.str(shortprefix, longprefix))) + { + break; + } + } + } + } + + if (optionType == OptionType::LongFlag && allowJoinedLongValue) + { + const auto separator = longseparator.empty() ? chunk.npos : chunk.find(longseparator); + if (separator != chunk.npos) + { + std::string arg(chunk, 0, separator); + if (auto flag = this->Match(arg.substr(longprefix.size()))) + { + for (auto &choice : flag->HelpChoices(helpParams)) + { + AddCompletionReply(chunk, arg + longseparator + choice); + } + } + } + } else if (optionType == OptionType::ShortFlag && allowJoinedShortValue) + { + if (chunk.size() > shortprefix.size() + 1) + { + auto arg = chunk.at(shortprefix.size()); + //TODO: support -abcVALUE where a and b take no value + if (auto flag = this->Match(arg)) + { + for (auto &choice : flag->HelpChoices(helpParams)) + { + AddCompletionReply(chunk, shortprefix + arg + choice); + } + } + } + } + } + } + +#ifndef ARGS_NOEXCEPT + throw Completion(completion->Get()); +#else + return true; +#endif + } + + template + It Parse(It begin, It end) + { + bool terminated = false; + std::vector commands = GetCommands(); + + // Check all arg chunks + for (auto it = begin; it != end; ++it) + { + if (Complete(it, end)) + { + return end; + } + + const auto &chunk = *it; + + if (!terminated && chunk == terminator) + { + terminated = true; + } else if (!terminated && ParseOption(chunk) == OptionType::LongFlag) + { + if (!ParseLong(it, end)) + { + return it; + } + } else if (!terminated && ParseOption(chunk) == OptionType::ShortFlag) + { + if (!ParseShort(it, end)) + { + return it; + } + } else if (!terminated && !commands.empty()) + { + auto itCommand = std::find_if(commands.begin(), commands.end(), [&chunk](Command *c) { return c->Name() == chunk; }); + if (itCommand == commands.end()) + { + const std::string errorMessage("Unknown command: " + chunk); +#ifndef ARGS_NOEXCEPT + throw ParseError(errorMessage); +#else + error = Error::Parse; + errorMsg = errorMessage; + return it; +#endif + } + + SelectCommand(*itCommand); + + if (const auto &coroutine = GetCoroutine()) + { + ++it; + RaiiSubparser coro(*this, std::vector(it, end)); + coroutine(coro.Parser()); +#ifdef ARGS_NOEXCEPT + error = GetError(); + if (error != Error::None) + { + return end; + } + + if (!coro.Parser().IsParsed()) + { + error = Error::Usage; + return end; + } +#else + if (!coro.Parser().IsParsed()) + { + throw UsageError("Subparser::Parse was not called"); + } +#endif + + break; + } + + commands = GetCommands(); + } else + { + auto pos = GetNextPositional(); + if (pos) + { + pos->ParseValue(chunk); + + if (pos->KickOut()) + { + return ++it; + } + } else + { + const std::string errorMessage("Passed in argument, but no positional arguments were ready to receive it: " + chunk); +#ifndef ARGS_NOEXCEPT + throw ParseError(errorMessage); +#else + error = Error::Parse; + errorMsg = errorMessage; + return it; +#endif + } + } + + if (!readCompletion && completion != nullptr && completion->Matched()) + { +#ifdef ARGS_NOEXCEPT + error = Error::Completion; +#endif + readCompletion = true; + ++it; + const auto argsLeft = static_cast(std::distance(it, end)); + if (completion->cword == 0 || argsLeft <= 1 || completion->cword >= argsLeft) + { +#ifndef ARGS_NOEXCEPT + throw Completion(""); +#endif + } + + std::vector curArgs(++it, end); + curArgs.resize(completion->cword); + + if (completion->syntax == "bash") + { + // bash tokenizes --flag=value as --flag=value + for (size_t idx = 0; idx < curArgs.size(); ) + { + if (idx > 0 && curArgs[idx] == "=") + { + curArgs[idx - 1] += "="; + // Avoid warnings from -Wsign-conversion + const auto signedIdx = static_cast(idx); + if (idx + 1 < curArgs.size()) + { + curArgs[idx - 1] += curArgs[idx + 1]; + curArgs.erase(curArgs.begin() + signedIdx, curArgs.begin() + signedIdx + 2); + } else + { + curArgs.erase(curArgs.begin() + signedIdx); + } + } else + { + ++idx; + } + } + + } +#ifndef ARGS_NOEXCEPT + try + { + Parse(curArgs.begin(), curArgs.end()); + throw Completion(""); + } + catch (Completion &) + { + throw; + } + catch (args::Error&) + { + throw Completion(""); + } +#else + return Parse(curArgs.begin(), curArgs.end()); +#endif + } + } + + Validate(shortprefix, longprefix); + return end; + } + + public: + HelpParams helpParams; + + ArgumentParser(const std::string &description_, const std::string &epilog_ = std::string()) + { + Description(description_); + Epilog(epilog_); + LongPrefix("--"); + ShortPrefix("-"); + LongSeparator("="); + Terminator("--"); + SetArgumentSeparations(true, true, true, true); + matched = true; + } + + void AddCompletion(CompletionFlag &completionFlag) + { + completion = &completionFlag; + Add(completionFlag); + } + + /** The program name for help generation + */ + const std::string &Prog() const + { return helpParams.programName; } + /** The program name for help generation + */ + void Prog(const std::string &prog_) + { this->helpParams.programName = prog_; } + + /** The prefix for long flags + */ + const std::string &LongPrefix() const + { return longprefix; } + /** The prefix for long flags + */ + void LongPrefix(const std::string &longprefix_) + { + this->longprefix = longprefix_; + this->helpParams.longPrefix = longprefix_; + } + + /** The prefix for short flags + */ + const std::string &ShortPrefix() const + { return shortprefix; } + /** The prefix for short flags + */ + void ShortPrefix(const std::string &shortprefix_) + { + this->shortprefix = shortprefix_; + this->helpParams.shortPrefix = shortprefix_; + } + + /** The separator for long flags + */ + const std::string &LongSeparator() const + { return longseparator; } + /** The separator for long flags + */ + void LongSeparator(const std::string &longseparator_) + { + if (longseparator_.empty()) + { + const std::string errorMessage("longseparator can not be set to empty"); +#ifdef ARGS_NOEXCEPT + error = Error::Usage; + errorMsg = errorMessage; +#else + throw UsageError(errorMessage); +#endif + } else + { + this->longseparator = longseparator_; + this->helpParams.longSeparator = allowJoinedLongValue ? longseparator_ : " "; + } + } + + /** The terminator that forcibly separates flags from positionals + */ + const std::string &Terminator() const + { return terminator; } + /** The terminator that forcibly separates flags from positionals + */ + void Terminator(const std::string &terminator_) + { this->terminator = terminator_; } + + /** Get the current argument separation parameters. + * + * See SetArgumentSeparations for details on what each one means. + */ + void GetArgumentSeparations( + bool &allowJoinedShortValue_, + bool &allowJoinedLongValue_, + bool &allowSeparateShortValue_, + bool &allowSeparateLongValue_) const + { + allowJoinedShortValue_ = this->allowJoinedShortValue; + allowJoinedLongValue_ = this->allowJoinedLongValue; + allowSeparateShortValue_ = this->allowSeparateShortValue; + allowSeparateLongValue_ = this->allowSeparateLongValue; + } + + /** Change allowed option separation. + * + * \param allowJoinedShortValue_ Allow a short flag that accepts an argument to be passed its argument immediately next to it (ie. in the same argv field) + * \param allowJoinedLongValue_ Allow a long flag that accepts an argument to be passed its argument separated by the longseparator (ie. in the same argv field) + * \param allowSeparateShortValue_ Allow a short flag that accepts an argument to be passed its argument separated by whitespace (ie. in the next argv field) + * \param allowSeparateLongValue_ Allow a long flag that accepts an argument to be passed its argument separated by whitespace (ie. in the next argv field) + */ + void SetArgumentSeparations( + const bool allowJoinedShortValue_, + const bool allowJoinedLongValue_, + const bool allowSeparateShortValue_, + const bool allowSeparateLongValue_) + { + this->allowJoinedShortValue = allowJoinedShortValue_; + this->allowJoinedLongValue = allowJoinedLongValue_; + this->allowSeparateShortValue = allowSeparateShortValue_; + this->allowSeparateLongValue = allowSeparateLongValue_; + + this->helpParams.longSeparator = allowJoinedLongValue ? longseparator : " "; + this->helpParams.shortSeparator = allowJoinedShortValue ? "" : " "; + } + + /** Pass the help menu into an ostream + */ + void Help(std::ostream &help_) const + { + auto &command = SelectedCommand(); + const auto &commandDescription = command.Description().empty() ? command.Help() : command.Description(); + const auto description_text = Wrap(commandDescription, helpParams.width - helpParams.descriptionindent); + const auto epilog_text = Wrap(command.Epilog(), helpParams.width - helpParams.descriptionindent); + + const bool hasoptions = command.HasFlag(); + const bool hasarguments = command.HasPositional(); + + std::vector prognameline; + prognameline.push_back(helpParams.usageString); + prognameline.push_back(Prog()); + auto commandProgLine = command.GetProgramLine(helpParams); + prognameline.insert(prognameline.end(), commandProgLine.begin(), commandProgLine.end()); + + const auto proglines = Wrap(prognameline.begin(), prognameline.end(), + helpParams.width - (helpParams.progindent + helpParams.progtailindent), + helpParams.width - helpParams.progindent); + auto progit = std::begin(proglines); + if (progit != std::end(proglines)) + { + help_ << std::string(helpParams.progindent, ' ') << *progit << '\n'; + ++progit; + } + for (; progit != std::end(proglines); ++progit) + { + help_ << std::string(helpParams.progtailindent, ' ') << *progit << '\n'; + } + + help_ << '\n'; + + if (!description_text.empty()) + { + for (const auto &line: description_text) + { + help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n"; + } + help_ << "\n"; + } + + bool lastDescriptionIsNewline = false; + + if (!helpParams.optionsString.empty()) + { + help_ << std::string(helpParams.progindent, ' ') << helpParams.optionsString << "\n\n"; + } + + for (const auto &desc: command.GetDescription(helpParams, 0)) + { + lastDescriptionIsNewline = std::get<0>(desc).empty() && std::get<1>(desc).empty(); + const auto groupindent = std::get<2>(desc) * helpParams.eachgroupindent; + const auto flags = Wrap(std::get<0>(desc), helpParams.width - (helpParams.flagindent + helpParams.helpindent + helpParams.gutter)); + const auto info = Wrap(std::get<1>(desc), helpParams.width - (helpParams.helpindent + groupindent)); + + std::string::size_type flagssize = 0; + for (auto flagsit = std::begin(flags); flagsit != std::end(flags); ++flagsit) + { + if (flagsit != std::begin(flags)) + { + help_ << '\n'; + } + help_ << std::string(groupindent + helpParams.flagindent, ' ') << *flagsit; + flagssize = Glyphs(*flagsit); + } + + auto infoit = std::begin(info); + // groupindent is on both sides of this inequality, and therefore can be removed + if ((helpParams.flagindent + flagssize + helpParams.gutter) > helpParams.helpindent || infoit == std::end(info) || helpParams.addNewlineBeforeDescription) + { + help_ << '\n'; + } else + { + // groupindent is on both sides of the minus sign, and therefore doesn't actually need to be in here + help_ << std::string(helpParams.helpindent - (helpParams.flagindent + flagssize), ' ') << *infoit << '\n'; + ++infoit; + } + for (; infoit != std::end(info); ++infoit) + { + help_ << std::string(groupindent + helpParams.helpindent, ' ') << *infoit << '\n'; + } + } + if (hasoptions && hasarguments && helpParams.showTerminator) + { + lastDescriptionIsNewline = false; + for (const auto &item: Wrap(std::string("\"") + terminator + "\" can be used to terminate flag options and force all following arguments to be treated as positional options", helpParams.width - helpParams.flagindent)) + { + help_ << std::string(helpParams.flagindent, ' ') << item << '\n'; + } + } + + if (!lastDescriptionIsNewline) + { + help_ << "\n"; + } + + for (const auto &line: epilog_text) + { + help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n"; + } + } + + /** Generate a help menu as a string. + * + * \return the help text as a single string + */ + std::string Help() const + { + std::ostringstream help_; + Help(help_); + return help_.str(); + } + + virtual void Reset() noexcept override + { + Command::Reset(); + matched = true; + readCompletion = false; + } + + /** Parse all arguments. + * + * \param begin an iterator to the beginning of the argument list + * \param end an iterator to the past-the-end element of the argument list + * \return the iterator after the last parsed value. Only useful for kick-out + */ + template + It ParseArgs(It begin, It end) + { + // Reset all Matched statuses and errors + Reset(); +#ifdef ARGS_NOEXCEPT + error = GetError(); + if (error != Error::None) + { + return end; + } +#endif + return Parse(begin, end); + } + + /** Parse all arguments. + * + * \param args an iterable of the arguments + * \return the iterator after the last parsed value. Only useful for kick-out + */ + template + auto ParseArgs(const T &args) -> decltype(std::begin(args)) + { + return ParseArgs(std::begin(args), std::end(args)); + } + + /** Convenience function to parse the CLI from argc and argv + * + * Just assigns the program name and vectorizes arguments for passing into ParseArgs() + * + * \return whether or not all arguments were parsed. This works for detecting kick-out, but is generally useless as it can't do anything with it. + */ + bool ParseCLI(const int argc, const char * const * argv) + { + if (Prog().empty()) + { + Prog(argv[0]); + } + const std::vector args(argv + 1, argv + argc); + return ParseArgs(args) == std::end(args); + } + + template + bool ParseCLI(const T &args) + { + return ParseArgs(args) == std::end(args); + } + }; + + inline Command::RaiiSubparser::RaiiSubparser(ArgumentParser &parser_, std::vector args_) + : command(parser_.SelectedCommand()), parser(std::move(args_), parser_, command, parser_.helpParams), oldSubparser(command.subparser) + { + command.subparser = &parser; + } + + inline Command::RaiiSubparser::RaiiSubparser(const Command &command_, const HelpParams ¶ms_): command(command_), parser(command, params_), oldSubparser(command.subparser) + { + command.subparser = &parser; + } + + inline void Subparser::Parse() + { + isParsed = true; + Reset(); + command.subparserDescription = GetDescription(helpParams, 0); + command.subparserHasFlag = HasFlag(); + command.subparserHasPositional = HasPositional(); + command.subparserHasCommand = HasCommand(); + command.subparserProgramLine = GetProgramLine(helpParams); + if (parser == nullptr) + { +#ifndef ARGS_NOEXCEPT + throw args::SubparserError(); +#else + error = Error::Subparser; + return; +#endif + } + + auto it = parser->Parse(args.begin(), args.end()); + command.Validate(parser->ShortPrefix(), parser->LongPrefix()); + kicked.assign(it, args.end()); + +#ifdef ARGS_NOEXCEPT + command.subparserError = GetError(); +#endif + } + + inline std::ostream &operator<<(std::ostream &os, const ArgumentParser &parser) + { + parser.Help(os); + return os; + } + + /** Boolean argument matcher + */ + class Flag : public FlagBase + { + public: + Flag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_): FlagBase(name_, help_, std::move(matcher_), options_) + { + group_.Add(*this); + } + + Flag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false): Flag(group_, name_, help_, std::move(matcher_), extraError_ ? Options::Single : Options::None) + { + } + + virtual ~Flag() {} + + /** Get whether this was matched + */ + bool Get() const + { + return Matched(); + } + + virtual Nargs NumberOfArguments() const noexcept override + { + return 0; + } + + virtual void ParseValue(const std::vector&) override + { + } + }; + + /** Help flag class + * + * Works like a regular flag, but throws an instance of Help when it is matched + */ + class HelpFlag : public Flag + { + public: + HelpFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_ = {}): Flag(group_, name_, help_, std::move(matcher_), options_) {} + + virtual ~HelpFlag() {} + + virtual void ParseValue(const std::vector &) + { +#ifdef ARGS_NOEXCEPT + error = Error::Help; + errorMsg = Name(); +#else + throw Help(Name()); +#endif + } + + /** Get whether this was matched + */ + bool Get() const noexcept + { + return Matched(); + } + }; + + /** A flag class that simply counts the number of times it's matched + */ + class CounterFlag : public Flag + { + private: + const int startcount; + int count; + + public: + CounterFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const int startcount_ = 0, Options options_ = {}): + Flag(group_, name_, help_, std::move(matcher_), options_), startcount(startcount_), count(startcount_) {} + + virtual ~CounterFlag() {} + + virtual FlagBase *Match(const EitherFlag &arg) override + { + auto me = FlagBase::Match(arg); + if (me) + { + ++count; + } + return me; + } + + /** Get the count + */ + int &Get() noexcept + { + return count; + } + + int &operator *() noexcept { + return count; + } + + const int &operator *() const noexcept { + return count; + } + + virtual void Reset() noexcept override + { + FlagBase::Reset(); + count = startcount; + } + }; + + /** A flag class that calls a function when it's matched + */ + class ActionFlag : public FlagBase + { + private: + std::function &)> action; + Nargs nargs; + + public: + ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Nargs nargs_, std::function &)> action_, Options options_ = {}): + FlagBase(name_, help_, std::move(matcher_), options_), action(std::move(action_)), nargs(nargs_) + { + group_.Add(*this); + } + + ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, std::function action_, Options options_ = {}): + FlagBase(name_, help_, std::move(matcher_), options_), nargs(1) + { + group_.Add(*this); + action = [action_](const std::vector &a) { return action_(a.at(0)); }; + } + + ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, std::function action_, Options options_ = {}): + FlagBase(name_, help_, std::move(matcher_), options_), nargs(0) + { + group_.Add(*this); + action = [action_](const std::vector &) { return action_(); }; + } + + virtual Nargs NumberOfArguments() const noexcept override + { return nargs; } + + virtual void ParseValue(const std::vector &value) override + { action(value); } + }; + + /** A default Reader class for argument classes + * + * If destination type is assignable to std::string it uses an assignment to std::string. + * Otherwise ValueReader simply uses a std::istringstream to read into the destination type, and + * raises a ParseError if there are any characters left. + */ + struct ValueReader + { + template + typename std::enable_if::value, bool>::type + operator ()(const std::string &name, const std::string &value, T &destination) + { + std::istringstream ss(value); + bool failed = !(ss >> destination); + + if (!failed) + { + ss >> std::ws; + } + + if (ss.rdbuf()->in_avail() > 0 || failed) + { +#ifdef ARGS_NOEXCEPT + (void)name; + return false; +#else + std::ostringstream problem; + problem << "Argument '" << name << "' received invalid value type '" << value << "'"; + throw ParseError(problem.str()); +#endif + } + return true; + } + + template + typename std::enable_if::value, bool>::type + operator()(const std::string &, const std::string &value, T &destination) + { + destination = value; + return true; + } + }; + + /** An argument-accepting flag class + * + * \tparam T the type to extract the argument as + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + */ + template < + typename T, + typename Reader = ValueReader> + class ValueFlag : public ValueFlagBase + { + protected: + T value; + T defaultValue; + + virtual std::string GetDefaultString(const HelpParams&) const override + { + return detail::ToString(defaultValue); + } + + private: + Reader reader; + + public: + + ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_, Options options_): ValueFlagBase(name_, help_, std::move(matcher_), options_), value(defaultValue_), defaultValue(defaultValue_) + { + group_.Add(*this); + } + + ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_ = T(), const bool extraError_ = false): ValueFlag(group_, name_, help_, std::move(matcher_), defaultValue_, extraError_ ? Options::Single : Options::None) + { + } + + ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_): ValueFlag(group_, name_, help_, std::move(matcher_), T(), options_) + { + } + + virtual ~ValueFlag() {} + + virtual void ParseValue(const std::vector &values_) override + { + const std::string &value_ = values_.at(0); + +#ifdef ARGS_NOEXCEPT + if (!reader(name, value_, this->value)) + { + error = Error::Parse; + } +#else + reader(name, value_, this->value); +#endif + } + + virtual void Reset() noexcept override + { + ValueFlagBase::Reset(); + value = defaultValue; + } + + /** Get the value + */ + T &Get() noexcept + { + return value; + } + + /** Get the value + */ + T &operator *() noexcept + { + return value; + } + + /** Get the value + */ + const T &operator *() const noexcept + { + return value; + } + + /** Get the value + */ + T *operator ->() noexcept + { + return &value; + } + + /** Get the value + */ + const T *operator ->() const noexcept + { + return &value; + } + + /** Get the default value + */ + const T &GetDefault() noexcept + { + return defaultValue; + } + }; + + /** An optional argument-accepting flag class + * + * \tparam T the type to extract the argument as + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + */ + template < + typename T, + typename Reader = ValueReader> + class ImplicitValueFlag : public ValueFlag + { + protected: + T implicitValue; + + public: + + ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &implicitValue_, const T &defaultValue_ = T(), Options options_ = {}) + : ValueFlag(group_, name_, help_, std::move(matcher_), defaultValue_, options_), implicitValue(implicitValue_) + { + } + + ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_ = T(), Options options_ = {}) + : ValueFlag(group_, name_, help_, std::move(matcher_), defaultValue_, options_), implicitValue(defaultValue_) + { + } + + ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) + : ValueFlag(group_, name_, help_, std::move(matcher_), {}, options_), implicitValue() + { + } + + virtual ~ImplicitValueFlag() {} + + virtual Nargs NumberOfArguments() const noexcept override + { + return {0, 1}; + } + + virtual void ParseValue(const std::vector &value_) override + { + if (value_.empty()) + { + this->value = implicitValue; + } else + { + ValueFlag::ParseValue(value_); + } + } + }; + + /** A variadic arguments accepting flag class + * + * \tparam T the type to extract the argument as + * \tparam List the list type that houses the values + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + */ + template < + typename T, + template class List = detail::vector, + typename Reader = ValueReader> + class NargsValueFlag : public FlagBase + { + protected: + + List values; + const List defaultValues; + Nargs nargs; + Reader reader; + + public: + + typedef List Container; + typedef T value_type; + typedef typename Container::allocator_type allocator_type; + typedef typename Container::pointer pointer; + typedef typename Container::const_pointer const_pointer; + typedef T& reference; + typedef const T& const_reference; + typedef typename Container::size_type size_type; + typedef typename Container::difference_type difference_type; + typedef typename Container::iterator iterator; + typedef typename Container::const_iterator const_iterator; + typedef std::reverse_iterator reverse_iterator; + typedef std::reverse_iterator const_reverse_iterator; + + NargsValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Nargs nargs_, const List &defaultValues_ = {}, Options options_ = {}) + : FlagBase(name_, help_, std::move(matcher_), options_), values(defaultValues_), defaultValues(defaultValues_),nargs(nargs_) + { + group_.Add(*this); + } + + virtual ~NargsValueFlag() {} + + virtual Nargs NumberOfArguments() const noexcept override + { + return nargs; + } + + virtual void ParseValue(const std::vector &values_) override + { + values.clear(); + + for (const std::string &value : values_) + { + T v; +#ifdef ARGS_NOEXCEPT + if (!reader(name, value, v)) + { + error = Error::Parse; + } +#else + reader(name, value, v); +#endif + values.insert(std::end(values), v); + } + } + + List &Get() noexcept + { + return values; + } + + /** Get the value + */ + List &operator *() noexcept + { + return values; + } + + /** Get the values + */ + const List &operator *() const noexcept + { + return values; + } + + /** Get the values + */ + List *operator ->() noexcept + { + return &values; + } + + /** Get the values + */ + const List *operator ->() const noexcept + { + return &values; + } + + iterator begin() noexcept + { + return values.begin(); + } + + const_iterator begin() const noexcept + { + return values.begin(); + } + + const_iterator cbegin() const noexcept + { + return values.cbegin(); + } + + iterator end() noexcept + { + return values.end(); + } + + const_iterator end() const noexcept + { + return values.end(); + } + + const_iterator cend() const noexcept + { + return values.cend(); + } + + virtual void Reset() noexcept override + { + FlagBase::Reset(); + values = defaultValues; + } + + virtual FlagBase *Match(const EitherFlag &arg) override + { + const bool wasMatched = Matched(); + auto me = FlagBase::Match(arg); + if (me && !wasMatched) + { + values.clear(); + } + return me; + } + }; + + /** An argument-accepting flag class that pushes the found values into a list + * + * \tparam T the type to extract the argument as + * \tparam List the list type that houses the values + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + */ + template < + typename T, + template class List = detail::vector, + typename Reader = ValueReader> + class ValueFlagList : public ValueFlagBase + { + private: + using Container = List; + Container values; + const Container defaultValues; + Reader reader; + + public: + + typedef T value_type; + typedef typename Container::allocator_type allocator_type; + typedef typename Container::pointer pointer; + typedef typename Container::const_pointer const_pointer; + typedef T& reference; + typedef const T& const_reference; + typedef typename Container::size_type size_type; + typedef typename Container::difference_type difference_type; + typedef typename Container::iterator iterator; + typedef typename Container::const_iterator const_iterator; + typedef std::reverse_iterator reverse_iterator; + typedef std::reverse_iterator const_reverse_iterator; + + ValueFlagList(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Container &defaultValues_ = Container(), Options options_ = {}): + ValueFlagBase(name_, help_, std::move(matcher_), options_), values(defaultValues_), defaultValues(defaultValues_) + { + group_.Add(*this); + } + + virtual ~ValueFlagList() {} + + virtual void ParseValue(const std::vector &values_) override + { + const std::string &value_ = values_.at(0); + + T v; +#ifdef ARGS_NOEXCEPT + if (!reader(name, value_, v)) + { + error = Error::Parse; + } +#else + reader(name, value_, v); +#endif + values.insert(std::end(values), v); + } + + /** Get the values + */ + Container &Get() noexcept + { + return values; + } + + /** Get the value + */ + Container &operator *() noexcept + { + return values; + } + + /** Get the values + */ + const Container &operator *() const noexcept + { + return values; + } + + /** Get the values + */ + Container *operator ->() noexcept + { + return &values; + } + + /** Get the values + */ + const Container *operator ->() const noexcept + { + return &values; + } + + virtual std::string Name() const override + { + return name + std::string("..."); + } + + virtual void Reset() noexcept override + { + ValueFlagBase::Reset(); + values = defaultValues; + } + + virtual FlagBase *Match(const EitherFlag &arg) override + { + const bool wasMatched = Matched(); + auto me = FlagBase::Match(arg); + if (me && !wasMatched) + { + values.clear(); + } + return me; + } + + iterator begin() noexcept + { + return values.begin(); + } + + const_iterator begin() const noexcept + { + return values.begin(); + } + + const_iterator cbegin() const noexcept + { + return values.cbegin(); + } + + iterator end() noexcept + { + return values.end(); + } + + const_iterator end() const noexcept + { + return values.end(); + } + + const_iterator cend() const noexcept + { + return values.cend(); + } + }; + + /** A mapping value flag class + * + * \tparam K the type to extract the argument as + * \tparam T the type to store the result as + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + * \tparam Map The Map type. Should operate like std::map or std::unordered_map + */ + template < + typename K, + typename T, + typename Reader = ValueReader, + template class Map = detail::unordered_map> + class MapFlag : public ValueFlagBase + { + private: + const Map map; + T value; + const T defaultValue; + Reader reader; + + protected: + virtual std::vector GetChoicesStrings(const HelpParams &) const override + { + return detail::MapKeysToStrings(map); + } + + public: + + MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, const T &defaultValue_, Options options_): ValueFlagBase(name_, help_, std::move(matcher_), options_), map(map_), value(defaultValue_), defaultValue(defaultValue_) + { + group_.Add(*this); + } + + MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, const T &defaultValue_ = T(), const bool extraError_ = false): MapFlag(group_, name_, help_, std::move(matcher_), map_, defaultValue_, extraError_ ? Options::Single : Options::None) + { + } + + MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, Options options_): MapFlag(group_, name_, help_, std::move(matcher_), map_, T(), options_) + { + } + + virtual ~MapFlag() {} + + virtual void ParseValue(const std::vector &values_) override + { + const std::string &value_ = values_.at(0); + + K key; +#ifdef ARGS_NOEXCEPT + if (!reader(name, value_, key)) + { + error = Error::Parse; + } +#else + reader(name, value_, key); +#endif + auto it = map.find(key); + if (it == std::end(map)) + { + std::ostringstream problem; + problem << "Could not find key '" << key << "' in map for arg '" << name << "'"; +#ifdef ARGS_NOEXCEPT + error = Error::Map; + errorMsg = problem.str(); +#else + throw MapError(problem.str()); +#endif + } else + { + this->value = it->second; + } + } + + /** Get the value + */ + T &Get() noexcept + { + return value; + } + + /** Get the value + */ + T &operator *() noexcept + { + return value; + } + + /** Get the value + */ + const T &operator *() const noexcept + { + return value; + } + + /** Get the value + */ + T *operator ->() noexcept + { + return &value; + } + + /** Get the value + */ + const T *operator ->() const noexcept + { + return &value; + } + + virtual void Reset() noexcept override + { + ValueFlagBase::Reset(); + value = defaultValue; + } + }; + + /** A mapping value flag list class + * + * \tparam K the type to extract the argument as + * \tparam T the type to store the result as + * \tparam List the list type that houses the values + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + * \tparam Map The Map type. Should operate like std::map or std::unordered_map + */ + template < + typename K, + typename T, + template class List = detail::vector, + typename Reader = ValueReader, + template class Map = detail::unordered_map> + class MapFlagList : public ValueFlagBase + { + private: + using Container = List; + const Map map; + Container values; + const Container defaultValues; + Reader reader; + + protected: + virtual std::vector GetChoicesStrings(const HelpParams &) const override + { + return detail::MapKeysToStrings(map); + } + + public: + typedef T value_type; + typedef typename Container::allocator_type allocator_type; + typedef typename Container::pointer pointer; + typedef typename Container::const_pointer const_pointer; + typedef T& reference; + typedef const T& const_reference; + typedef typename Container::size_type size_type; + typedef typename Container::difference_type difference_type; + typedef typename Container::iterator iterator; + typedef typename Container::const_iterator const_iterator; + typedef std::reverse_iterator reverse_iterator; + typedef std::reverse_iterator const_reverse_iterator; + + MapFlagList(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, const Container &defaultValues_ = Container()): ValueFlagBase(name_, help_, std::move(matcher_)), map(map_), values(defaultValues_), defaultValues(defaultValues_) + { + group_.Add(*this); + } + + virtual ~MapFlagList() {} + + virtual void ParseValue(const std::vector &values_) override + { + const std::string &value = values_.at(0); + + K key; +#ifdef ARGS_NOEXCEPT + if (!reader(name, value, key)) + { + error = Error::Parse; + } +#else + reader(name, value, key); +#endif + auto it = map.find(key); + if (it == std::end(map)) + { + std::ostringstream problem; + problem << "Could not find key '" << key << "' in map for arg '" << name << "'"; +#ifdef ARGS_NOEXCEPT + error = Error::Map; + errorMsg = problem.str(); +#else + throw MapError(problem.str()); +#endif + } else + { + this->values.emplace_back(it->second); + } + } + + /** Get the value + */ + Container &Get() noexcept + { + return values; + } + + /** Get the value + */ + Container &operator *() noexcept + { + return values; + } + + /** Get the values + */ + const Container &operator *() const noexcept + { + return values; + } + + /** Get the values + */ + Container *operator ->() noexcept + { + return &values; + } + + /** Get the values + */ + const Container *operator ->() const noexcept + { + return &values; + } + + virtual std::string Name() const override + { + return name + std::string("..."); + } + + virtual void Reset() noexcept override + { + ValueFlagBase::Reset(); + values = defaultValues; + } + + virtual FlagBase *Match(const EitherFlag &arg) override + { + const bool wasMatched = Matched(); + auto me = FlagBase::Match(arg); + if (me && !wasMatched) + { + values.clear(); + } + return me; + } + + iterator begin() noexcept + { + return values.begin(); + } + + const_iterator begin() const noexcept + { + return values.begin(); + } + + const_iterator cbegin() const noexcept + { + return values.cbegin(); + } + + iterator end() noexcept + { + return values.end(); + } + + const_iterator end() const noexcept + { + return values.end(); + } + + const_iterator cend() const noexcept + { + return values.cend(); + } + }; + + /** A positional argument class + * + * \tparam T the type to extract the argument as + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + */ + template < + typename T, + typename Reader = ValueReader> + class Positional : public PositionalBase + { + private: + T value; + const T defaultValue; + Reader reader; + public: + Positional(Group &group_, const std::string &name_, const std::string &help_, const T &defaultValue_ = T(), Options options_ = {}): PositionalBase(name_, help_, options_), value(defaultValue_), defaultValue(defaultValue_) + { + group_.Add(*this); + } + + Positional(Group &group_, const std::string &name_, const std::string &help_, Options options_): Positional(group_, name_, help_, T(), options_) + { + } + + virtual ~Positional() {} + + virtual void ParseValue(const std::string &value_) override + { +#ifdef ARGS_NOEXCEPT + if (!reader(name, value_, this->value)) + { + error = Error::Parse; + } +#else + reader(name, value_, this->value); +#endif + ready = false; + matched = true; + } + + /** Get the value + */ + T &Get() noexcept + { + return value; + } + + /** Get the value + */ + T &operator *() noexcept + { + return value; + } + + /** Get the value + */ + const T &operator *() const noexcept + { + return value; + } + + /** Get the value + */ + T *operator ->() noexcept + { + return &value; + } + + /** Get the value + */ + const T *operator ->() const noexcept + { + return &value; + } + + virtual void Reset() noexcept override + { + PositionalBase::Reset(); + value = defaultValue; + } + }; + + /** A positional argument class that pushes the found values into a list + * + * \tparam T the type to extract the argument as + * \tparam List the list type that houses the values + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + */ + template < + typename T, + template class List = detail::vector, + typename Reader = ValueReader> + class PositionalList : public PositionalBase + { + private: + using Container = List; + Container values; + const Container defaultValues; + Reader reader; + + public: + typedef T value_type; + typedef typename Container::allocator_type allocator_type; + typedef typename Container::pointer pointer; + typedef typename Container::const_pointer const_pointer; + typedef T& reference; + typedef const T& const_reference; + typedef typename Container::size_type size_type; + typedef typename Container::difference_type difference_type; + typedef typename Container::iterator iterator; + typedef typename Container::const_iterator const_iterator; + typedef std::reverse_iterator reverse_iterator; + typedef std::reverse_iterator const_reverse_iterator; + + PositionalList(Group &group_, const std::string &name_, const std::string &help_, const Container &defaultValues_ = Container(), Options options_ = {}): PositionalBase(name_, help_, options_), values(defaultValues_), defaultValues(defaultValues_) + { + group_.Add(*this); + } + + PositionalList(Group &group_, const std::string &name_, const std::string &help_, Options options_): PositionalList(group_, name_, help_, {}, options_) + { + } + + virtual ~PositionalList() {} + + virtual void ParseValue(const std::string &value_) override + { + T v; +#ifdef ARGS_NOEXCEPT + if (!reader(name, value_, v)) + { + error = Error::Parse; + } +#else + reader(name, value_, v); +#endif + values.insert(std::end(values), v); + matched = true; + } + + virtual std::string Name() const override + { + return name + std::string("..."); + } + + /** Get the values + */ + Container &Get() noexcept + { + return values; + } + + /** Get the value + */ + Container &operator *() noexcept + { + return values; + } + + /** Get the values + */ + const Container &operator *() const noexcept + { + return values; + } + + /** Get the values + */ + Container *operator ->() noexcept + { + return &values; + } + + /** Get the values + */ + const Container *operator ->() const noexcept + { + return &values; + } + + virtual void Reset() noexcept override + { + PositionalBase::Reset(); + values = defaultValues; + } + + virtual PositionalBase *GetNextPositional() override + { + const bool wasMatched = Matched(); + auto me = PositionalBase::GetNextPositional(); + if (me && !wasMatched) + { + values.clear(); + } + return me; + } + + iterator begin() noexcept + { + return values.begin(); + } + + const_iterator begin() const noexcept + { + return values.begin(); + } + + const_iterator cbegin() const noexcept + { + return values.cbegin(); + } + + iterator end() noexcept + { + return values.end(); + } + + const_iterator end() const noexcept + { + return values.end(); + } + + const_iterator cend() const noexcept + { + return values.cend(); + } + }; + + /** A positional argument mapping class + * + * \tparam K the type to extract the argument as + * \tparam T the type to store the result as + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + * \tparam Map The Map type. Should operate like std::map or std::unordered_map + */ + template < + typename K, + typename T, + typename Reader = ValueReader, + template class Map = detail::unordered_map> + class MapPositional : public PositionalBase + { + private: + const Map map; + T value; + const T defaultValue; + Reader reader; + + protected: + virtual std::vector GetChoicesStrings(const HelpParams &) const override + { + return detail::MapKeysToStrings(map); + } + + public: + + MapPositional(Group &group_, const std::string &name_, const std::string &help_, const Map &map_, const T &defaultValue_ = T(), Options options_ = {}): + PositionalBase(name_, help_, options_), map(map_), value(defaultValue_), defaultValue(defaultValue_) + { + group_.Add(*this); + } + + virtual ~MapPositional() {} + + virtual void ParseValue(const std::string &value_) override + { + K key; +#ifdef ARGS_NOEXCEPT + if (!reader(name, value_, key)) + { + error = Error::Parse; + } +#else + reader(name, value_, key); +#endif + auto it = map.find(key); + if (it == std::end(map)) + { + std::ostringstream problem; + problem << "Could not find key '" << key << "' in map for arg '" << name << "'"; +#ifdef ARGS_NOEXCEPT + error = Error::Map; + errorMsg = problem.str(); +#else + throw MapError(problem.str()); +#endif + } else + { + this->value = it->second; + ready = false; + matched = true; + } + } + + /** Get the value + */ + T &Get() noexcept + { + return value; + } + + /** Get the value + */ + T &operator *() noexcept + { + return value; + } + + /** Get the value + */ + const T &operator *() const noexcept + { + return value; + } + + /** Get the value + */ + T *operator ->() noexcept + { + return &value; + } + + /** Get the value + */ + const T *operator ->() const noexcept + { + return &value; + } + + virtual void Reset() noexcept override + { + PositionalBase::Reset(); + value = defaultValue; + } + }; + + /** A positional argument mapping list class + * + * \tparam K the type to extract the argument as + * \tparam T the type to store the result as + * \tparam List the list type that houses the values + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + * \tparam Map The Map type. Should operate like std::map or std::unordered_map + */ + template < + typename K, + typename T, + template class List = detail::vector, + typename Reader = ValueReader, + template class Map = detail::unordered_map> + class MapPositionalList : public PositionalBase + { + private: + using Container = List; + + const Map map; + Container values; + const Container defaultValues; + Reader reader; + + protected: + virtual std::vector GetChoicesStrings(const HelpParams &) const override + { + return detail::MapKeysToStrings(map); + } + + public: + typedef T value_type; + typedef typename Container::allocator_type allocator_type; + typedef typename Container::pointer pointer; + typedef typename Container::const_pointer const_pointer; + typedef T& reference; + typedef const T& const_reference; + typedef typename Container::size_type size_type; + typedef typename Container::difference_type difference_type; + typedef typename Container::iterator iterator; + typedef typename Container::const_iterator const_iterator; + typedef std::reverse_iterator reverse_iterator; + typedef std::reverse_iterator const_reverse_iterator; + + MapPositionalList(Group &group_, const std::string &name_, const std::string &help_, const Map &map_, const Container &defaultValues_ = Container(), Options options_ = {}): + PositionalBase(name_, help_, options_), map(map_), values(defaultValues_), defaultValues(defaultValues_) + { + group_.Add(*this); + } + + virtual ~MapPositionalList() {} + + virtual void ParseValue(const std::string &value_) override + { + K key; +#ifdef ARGS_NOEXCEPT + if (!reader(name, value_, key)) + { + error = Error::Parse; + } +#else + reader(name, value_, key); +#endif + auto it = map.find(key); + if (it == std::end(map)) + { + std::ostringstream problem; + problem << "Could not find key '" << key << "' in map for arg '" << name << "'"; +#ifdef ARGS_NOEXCEPT + error = Error::Map; + errorMsg = problem.str(); +#else + throw MapError(problem.str()); +#endif + } else + { + this->values.emplace_back(it->second); + matched = true; + } + } + + /** Get the value + */ + Container &Get() noexcept + { + return values; + } + + /** Get the value + */ + Container &operator *() noexcept + { + return values; + } + + /** Get the values + */ + const Container &operator *() const noexcept + { + return values; + } + + /** Get the values + */ + Container *operator ->() noexcept + { + return &values; + } + + /** Get the values + */ + const Container *operator ->() const noexcept + { + return &values; + } + + virtual std::string Name() const override + { + return name + std::string("..."); + } + + virtual void Reset() noexcept override + { + PositionalBase::Reset(); + values = defaultValues; + } + + virtual PositionalBase *GetNextPositional() override + { + const bool wasMatched = Matched(); + auto me = PositionalBase::GetNextPositional(); + if (me && !wasMatched) + { + values.clear(); + } + return me; + } + + iterator begin() noexcept + { + return values.begin(); + } + + const_iterator begin() const noexcept + { + return values.begin(); + } + + const_iterator cbegin() const noexcept + { + return values.cbegin(); + } + + iterator end() noexcept + { + return values.end(); + } + + const_iterator end() const noexcept + { + return values.end(); + } + + const_iterator cend() const noexcept + { + return values.cend(); + } + }; +} + +#endif diff --git a/lci/extra/dashun/dashun/dashun.vcxproj b/lci/extra/dashun/dashun/dashun.vcxproj new file mode 100644 index 0000000..990f988 --- /dev/null +++ b/lci/extra/dashun/dashun/dashun.vcxproj @@ -0,0 +1,141 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {a0e6a4c1-02b0-41f5-abb7-33c6e4fb97f6} + dashun + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + C:\local\boost_1_88_0;C:\Program Files\libuv\include;%(AdditionalIncludeDirectories) + + + Console + true + C:\local\boost_1_88_0\lib64-msvc-14.3;C:\Program Files\libuv;%(AdditionalLibraryDirectories) + libuv.lib;ntdll.lib;%(AdditionalDependencies) + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lci/extra/dashun/dashun/dashun.vcxproj.filters b/lci/extra/dashun/dashun/dashun.vcxproj.filters new file mode 100644 index 0000000..6fbc8d5 --- /dev/null +++ b/lci/extra/dashun/dashun/dashun.vcxproj.filters @@ -0,0 +1,36 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + 头文件 + + + 源文件 + + + + + 头文件 + + + 头文件 + + + 头文件 + + + \ No newline at end of file diff --git a/lci/extra/dashun/dashun/dashun.vcxproj.user b/lci/extra/dashun/dashun/dashun.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/lci/extra/dashun/dashun/dashun.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lci/extra/dashun/dashun_to/dashun_to.cpp b/lci/extra/dashun/dashun_to/dashun_to.cpp new file mode 100644 index 0000000..8a528d8 --- /dev/null +++ b/lci/extra/dashun/dashun_to/dashun_to.cpp @@ -0,0 +1,20 @@ +// dashun_to.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 +// + +#include + +int main() +{ + std::cout << "Hello World!\n"; +} + +// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单 +// 调试程序: F5 或调试 >“开始调试”菜单 + +// 入门使用技巧: +// 1. 使用解决方案资源管理器窗口添加/管理文件 +// 2. 使用团队资源管理器窗口连接到源代码管理 +// 3. 使用输出窗口查看生成输出和其他消息 +// 4. 使用错误列表窗口查看错误 +// 5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目 +// 6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件 diff --git a/lci/extra/dashun/dashun_to/dashun_to.vcxproj b/lci/extra/dashun/dashun_to/dashun_to.vcxproj new file mode 100644 index 0000000..bc91e10 --- /dev/null +++ b/lci/extra/dashun/dashun_to/dashun_to.vcxproj @@ -0,0 +1,131 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {04d7a85c-43bb-4696-b9c8-f79c2252eda1} + dashunto + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + + + + + \ No newline at end of file diff --git a/lci/extra/dashun/dashun_to/dashun_to.vcxproj.filters b/lci/extra/dashun/dashun_to/dashun_to.vcxproj.filters new file mode 100644 index 0000000..04e3d9d --- /dev/null +++ b/lci/extra/dashun/dashun_to/dashun_to.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + 源文件 + + + \ No newline at end of file diff --git a/lci/extra/dashun/dashun_to/dashun_to.vcxproj.user b/lci/extra/dashun/dashun_to/dashun_to.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/lci/extra/dashun/dashun_to/dashun_to.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lci/extra/dashun/regiest.bat b/lci/extra/dashun/regiest.bat new file mode 100644 index 0000000..b389762 --- /dev/null +++ b/lci/extra/dashun/regiest.bat @@ -0,0 +1 @@ +sc create ´ó˳½ű¾ binPath= "C:\Users\90999\Desktop\tws\dashun\x64\Debug\dashun.exe" start= auto DisplayName= "´ó˳½ű¾£¬½ø³ÌÍйܷþÎñ" \ No newline at end of file diff --git a/lci/extra/mgmt/README.md b/lci/extra/mgmt/README.md new file mode 100644 index 0000000..13cf2a9 --- /dev/null +++ b/lci/extra/mgmt/README.md @@ -0,0 +1,6 @@ +# mgmt +这是基于pmc模块的管理平台,负责对子系统进行管理。 + +## 放置秘钥 + +为了和pmc系统通信,我们需要在该目录下放置pmc的rsa私钥 \ No newline at end of file diff --git a/lci/extra/mgmt/_run.sh b/lci/extra/mgmt/_run.sh new file mode 100644 index 0000000..3ca6128 --- /dev/null +++ b/lci/extra/mgmt/_run.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd "$(dirname "$0")" || exit +bash ./_setup.sh +source ./.env/bin/activate +python main.py diff --git a/lci/extra/mgmt/_setup.sh b/lci/extra/mgmt/_setup.sh new file mode 100644 index 0000000..d8965bd --- /dev/null +++ b/lci/extra/mgmt/_setup.sh @@ -0,0 +1,7 @@ +#!/bin/bash +cd "$(dirname "$0")" || exit +python3 -m venv .env +#sudo chmod +x .env/bin/activate +source .env/bin/activate +pip install -r requirements.txt -i https://pypi.org/simple + diff --git a/lci/extra/mgmt/app/__init__.py b/lci/extra/mgmt/app/__init__.py new file mode 100644 index 0000000..a224de2 --- /dev/null +++ b/lci/extra/mgmt/app/__init__.py @@ -0,0 +1,14 @@ +from app.login.routes import login_bp +from app.log.routes import log_bp +from app.module.routes import module_bp +from app.process.routes import process_bp +from app.user.routes import user_bp + + +def register_blueprints(app): + # 默认访问家目录 + app.register_blueprint(login_bp, url_prefix='/') + app.register_blueprint(log_bp, url_prefix='/log') + app.register_blueprint(module_bp, url_prefix='/module') + app.register_blueprint(process_bp, url_prefix='/process') + app.register_blueprint(user_bp, url_prefix='/user') diff --git a/lci/extra/mgmt/app/login/__init__.py b/lci/extra/mgmt/app/login/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lci/extra/mgmt/app/login/routes.py b/lci/extra/mgmt/app/login/routes.py new file mode 100644 index 0000000..5365280 --- /dev/null +++ b/lci/extra/mgmt/app/login/routes.py @@ -0,0 +1,113 @@ +import datetime +import hashlib +from functools import wraps + +from pmc import DnfsComm +from dnfs import UsersTable, DnfsTable + +import jwt +from flask import Blueprint, render_template, request, redirect, url_for, current_app, make_response, jsonify + +login_bp = Blueprint('login', __name__, template_folder='templates/login') + + +comm = DnfsComm("http://localhost:8006") + + +def get_user_from_token(): + token = request.cookies.get('token') + if not token: + return None + try: + payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) + return payload # 包含 user、user_id、exp 等字段 + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + + +@login_bp.after_request +def add_cors_headers(response): + response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名 + response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization' + response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS' + return response + + +def token_required(f): + @wraps(f) + def decorated(*args, **kwargs): + # 从cookie中获取token + token = request.cookies.get('token') + if not token: + return redirect(url_for('login.index')) + try: + jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) + except jwt.ExpiredSignatureError: + return redirect(url_for('login.index')) + except jwt.InvalidTokenError: + return redirect(url_for('login.index')) + return f(*args, **kwargs) + return decorated + + +@login_bp.route('/') +def index(): + return render_template('index.html') + + +@login_bp.route('/api/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form.get('user-name') + password = request.form.get('user-passwd') + password = password.encode("utf-8") + hashed_password = hashlib.sha256(password).hexdigest() + try: + data = comm.get_and_trans(0) + dnfs_tab = DnfsTable(data) + users = dnfs_tab.query_by_title('Users') + table = UsersTable(users.content) + data = table.user_exists(username, hashed_password) + if not data: + return "用户登录失败", 401 + _user_id, _username, _password, _role = data + except DnfsComm.GetFailed: + return "获取数据失败", 500 + except DnfsTable.NotFound: + return "未找到用户表", 404 + except UsersTable.NotFound: + return "用户不存在", 401 + except Exception as e: + return str(e), 404 + # 生成 Token + token = jwt.encode({ + 'user_id': _user_id, + 'username': _username, + 'role': _role, + 'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=3) + }, current_app.config['SECRET_KEY'], algorithm='HS256') + + # 构造 JSON 响应体 + response = make_response(jsonify({ + "redirect": url_for('login.mgmt'), # 跳转路径 + "token": token + })) + + # 设置 Cookie + response.set_cookie('token', token) + return response + else: + return "登录失败" + + +@login_bp.route('/api/mgmt') +@token_required +def mgmt(): + payload = get_user_from_token() + if not payload: + return redirect(url_for('login.index')) + username = payload.get('username') + role = payload.get('role') + return render_template('mgmt.html', username=username, role=role) diff --git a/lci/extra/mgmt/app/module/__init__.py b/lci/extra/mgmt/app/module/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lci/extra/mgmt/app/module/routes.py b/lci/extra/mgmt/app/module/routes.py new file mode 100644 index 0000000..0f2df75 --- /dev/null +++ b/lci/extra/mgmt/app/module/routes.py @@ -0,0 +1,34 @@ +from pmc import PmcComm +from flask import Blueprint, render_template + +module_bp = Blueprint('module', __name__, template_folder='templates/features/module.html') + +pmc = PmcComm('http://localhost:8012') +pmc.Set_RSA_Pri_Key("private_key.pem") +pmc.Try_Get_AES_Key() + + +@module_bp.after_request +def add_cors_headers(response): + response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名 + response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization' + response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS' + return response + + +@module_bp.route('/api/mgmt/module', methods=['GET', 'POST']) +def module(): + data = pmc.list_mods() + modules = data.split('\n') + return render_template('features/module.html', modules=modules) + + +@module_bp.route('/create_module', methods=['GET', 'POST']) +def create_module(mod_name): + pass + + +@module_bp.route('/delete_module', methods=['GET', 'POST']) +def delete_module(mod_name): + pass + diff --git a/lci/extra/mgmt/app/process/__init__.py b/lci/extra/mgmt/app/process/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lci/extra/mgmt/app/process/routes.py b/lci/extra/mgmt/app/process/routes.py new file mode 100644 index 0000000..4723c4b --- /dev/null +++ b/lci/extra/mgmt/app/process/routes.py @@ -0,0 +1,53 @@ +from flask import Blueprint, render_template, jsonify, request +from pmc import PmcComm + + +process_bp = Blueprint('process', __name__, template_folder='templates/features/process.html') + +pmc = PmcComm('http://localhost:8012') +pmc.Set_RSA_Pri_Key("private_key.pem") +pmc.Try_Get_AES_Key() + + +@process_bp.after_request +def add_cors_headers(response): + response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名 + response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization' + response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS' + return response + + +@process_bp.route('/api/mgmt/process', methods=['GET', 'POST']) +def process(): + pid = [] + status = [] + order = [] + ls = pmc.get_list() + if ls != "": + ls_1 = ls.split('\n') + for l in ls_1: + i, j, k = l.split('\t') + pid.append(i) + status.append(j) + order.append(k) + data = [{"pid": p, "status": s, "order": o} for p, s, o in zip(pid, status, order)] + + return render_template('features/process.html', response=data) + + +@process_bp.route('/kill_process', methods=['POST']) +def kill_process(pid): + pmc.kill(pid) + return jsonify({"status": "success"}) + + +@process_bp.route('/clear_process', methods=['POST']) +def clear_process(): + pmc.clear() + return jsonify({"status": "success"}) + + +@process_bp.route('/create_process', methods=['POST']) +def create_process(name): + pmc.start(name) + return jsonify({"status": "success"}) diff --git a/lci/extra/mgmt/app/user/__init__.py b/lci/extra/mgmt/app/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lci/extra/mgmt/app/user/routes.py b/lci/extra/mgmt/app/user/routes.py new file mode 100644 index 0000000..b5b9f35 --- /dev/null +++ b/lci/extra/mgmt/app/user/routes.py @@ -0,0 +1,111 @@ +import jwt +import hashlib +from flask import Blueprint, render_template, request, current_app, jsonify +from pmc import DnfsComm +from dnfs import UsersTable, DnfsTable + +user_bp = Blueprint('user', __name__, template_folder='templates/features/user.html') + +comm = DnfsComm("http://localhost:8006") + + + +def get_user_from_token(): + token = request.cookies.get('token') + if not token: + return None + try: + payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) + return payload # 包含 user、user_id、exp 等字段 + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + + +def get_node(): + try: + data = comm.get_and_trans(0) + except DnfsComm.GetFailed: + return "获取数据失败", 500 + for item in data: + if item['title'] == 'Users': + node_id = item['id'] + node_content = item['content'] + return node_id, node_content + return None + + +@user_bp.after_request +def add_cors_headers(response): + response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名 + response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization' + response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS' + return response + + +@user_bp.route('/api/mgmt/user', methods=['GET', 'POST']) +def user(): + try: + data = comm.get_and_trans(0) + dnfs_tab = DnfsTable(data) + users = dnfs_tab.query_by_title('Users') + table = UsersTable(users.content) + crt_dict = lambda user: {'user_id': user.id, 'username': user.name, 'role': user.privileges} + user_list = [crt_dict(user) for user in table.arr] + except DnfsComm.GetFailed: + return "获取数据失败", 500 + except DnfsTable.NotFound: + return "未找到用户表", 404 + except UsersTable.NotFound: + return "用户不存在", 401 + except Exception as e: + return str(e), 404 + payload = get_user_from_token() + if not payload: + return jsonify({"error": "未授权访问"}), 401 + + user_id = payload.get('user_id') + role = payload.get('role') + + return render_template('features/user.html', + response=user_list, + current_user_id=user_id, + current_role=role) + + +@user_bp.route('/add_user', methods=['POST']) +def add_user(): + node_id, msg = get_node() + username = request.form.get('username') + password = request.form.get('password') + hashed_password = hashlib.sha256(password.encode("utf-8")).hexdigest() + role = "normal" + table = UsersTable(msg) + table.add_user(username, hashed_password, role) + table = str(table) + comm.upd(node_id, 0, 'Users', table) + return jsonify({"message": "用户添加成功"}) + + +@user_bp.route('/change_password', methods=['POST']) +def change_password(): + node_id, msg = get_node() + user_id = request.form.get('user_id') + new_password = request.form.get('new_password') + hashed_password = hashlib.sha256(new_password.encode("utf-8")).hexdigest() + table = UsersTable(msg) + table.change_password(int(user_id), hashed_password) + table = str(table) + comm.upd(node_id, 0, 'Users', table) + return jsonify({"message": "密码修改成功"}) + + +@user_bp.route('/delete_user', methods=['POST']) +def delete_user(user_id): + node_id, msg = get_node() + table = UsersTable(msg) + table.del_user(user_id) + table = str(table) + comm.upd(node_id, 0, 'Users', table) + return jsonify({"message": "用户删除成功"}) diff --git a/lci/extra/mgmt/main.py b/lci/extra/mgmt/main.py new file mode 100644 index 0000000..409c0fe --- /dev/null +++ b/lci/extra/mgmt/main.py @@ -0,0 +1,17 @@ +from app import register_blueprints +from flask import Flask +from waitress import serve + +# 初始化Flask应用 +app = Flask(__name__) + +# 设置一个密钥 +app.secret_key = 'secret_key' + +# 注册蓝图 +register_blueprints(app) + + +if __name__ == '__main__': + #app.run(host='0.0.0.0', port=8009, debug=True) + serve(app, host="0.0.0.0", port=8009) diff --git a/lci/extra/mgmt/requirements.txt b/lci/extra/mgmt/requirements.txt new file mode 100644 index 0000000..5e8107d --- /dev/null +++ b/lci/extra/mgmt/requirements.txt @@ -0,0 +1,5 @@ +../_pmc +../_dnfs +flask +PyJWT +waitress diff --git a/lci/extra/mgmt/static/features/log.css b/lci/extra/mgmt/static/features/log.css new file mode 100644 index 0000000..e69de29 diff --git a/lci/extra/mgmt/static/features/module.css b/lci/extra/mgmt/static/features/module.css new file mode 100644 index 0000000..6f768e1 --- /dev/null +++ b/lci/extra/mgmt/static/features/module.css @@ -0,0 +1,35 @@ +.top-bar-con { + width: 230px; + display: flex; + justify-content: space-between; +} + +.newBtn, +.flushBtn, +.removeBtn { + width: 84px; + height: 42px; + font-size: 16px; + font-weight: bold; + letter-spacing: 1px; + cursor: pointer; +} + +.newBtn { + background-color: #5cff007d; +} + +.flushBtn { + background-color: #5757577d; +} + +.removeBtn { + background-color: #ff00007d; + color: #000000b8; +} + +td { + width: 180px; + text-align: center; + font-size: 20px; +} \ No newline at end of file diff --git a/lci/extra/mgmt/static/features/process.css b/lci/extra/mgmt/static/features/process.css new file mode 100644 index 0000000..ad10d6b --- /dev/null +++ b/lci/extra/mgmt/static/features/process.css @@ -0,0 +1,74 @@ +.newBtn, +.flushBtn, +.stopBtn { + width: 84px; + height: 42px; + font-size: 16px; + font-weight: bold; + letter-spacing: 1px; + cursor: pointer; +} + +.newBtn { + background-color: #5cff007d; +} + +.flushBtn { + background-color: #5757577d; + margin-left: 60px; +} + +.stopBtn { + background-color: #ff00007d; +} + +.text, +.title { + font-size: 20px; +} + +td, +th { + width: 180px; + text-align: center; +} + +#inProgressText { + font-size: 20px; + margin-top: 5px; + letter-spacing: 2px; +} + +/* modal window style */ +.modal-con { + width: 100%; + height: 100%; + background-color: rgba(68, 94, 65, 0.77); + position: absolute; + left: 0px; + top: 0px; + display: none; + justify-content: center; + z-index: 1; +} + +.modal-con .window { + width: 200px; + height: 120px; + background-color: #5cff007d; + padding: 0px 10px; +} + +.modal-con .window .top-bar { + display: flex; + justify-content: end; +} + +.modal-con .window .top-bar .close-btn { + cursor: pointer; +} + +.modal-con .window .create-btn, +.modal-con .window .input .input-area { + margin-top: 5px; +} \ No newline at end of file diff --git a/lci/extra/mgmt/static/features/user.css b/lci/extra/mgmt/static/features/user.css new file mode 100644 index 0000000..faff9c6 --- /dev/null +++ b/lci/extra/mgmt/static/features/user.css @@ -0,0 +1,41 @@ +.top-bar-con { + width: 230px; + display: flex; + justify-content: space-between; +} + +td { + width: 180px; + text-align: center; + font-size: 20px; +} + +.addUserBtn, +.flushBtn, +.changePasswdBtn, +.deleteUserBtn { + width: 84px; + height: 42px; + font-size: 16px; + font-weight: bold; + letter-spacing: 1px; + cursor: pointer; +} + +.addUserBtn { + background-color: #5cff007d; +} + +.flushBtn { + background-color: #5757577d; +} + +.changePasswdBtn { + background-color: #e6db407d; + color: #fb0202b0; +} + +.deleteUserBtn { + background-color: #ff00007d; + color: #000000b8; +} \ No newline at end of file diff --git a/lci/extra/mgmt/static/index.css b/lci/extra/mgmt/static/index.css new file mode 100644 index 0000000..15a0402 --- /dev/null +++ b/lci/extra/mgmt/static/index.css @@ -0,0 +1,39 @@ +body { + display: flex; + justify-content: center; +} + +.main { + background-color: rgb(233, 233, 233); + padding: 20px 10px; +} + +.main .title { + font-weight: bold; +} + +.main .name-con, +.main .passwd-con { + display: flex; + justify-content: center; +} + +.main .name-con { + margin-top: 20px; +} + +.main .passwd-con { + margin-top: 10px; +} + +.main .name-con .user-name-text, +.main .passwd-con .user-passwd-text { + width: 100px; +} + +.main .login-con { + width: 100%; + display: flex; + justify-content: center; + margin-top: 10px; +} \ No newline at end of file diff --git a/lci/extra/mgmt/static/mgmt.css b/lci/extra/mgmt/static/mgmt.css new file mode 100644 index 0000000..607dc05 --- /dev/null +++ b/lci/extra/mgmt/static/mgmt.css @@ -0,0 +1,54 @@ +:root { + --listMargin: 15%; +} + +* { + margin: 0px; + padding: 0px; +} + +.top-con { + display: flex; +} + +.top-con >*{ + margin-left: 25px; + font-size: 20px; +} + +.top-con div button { + font-size: 16px; +} + +.main { + width: 100%; + height: 600px; + display: flex; +} + +.feature-list { + width: 150px; + height: 100%; + background-color: #FFFFCC; + padding: 0px 10px 0px 10px; + overflow-y: scroll; +} + +.feature-list .item { + width: 100%; + height: 50px; + display: flex; + justify-content: center; + align-items: center; + background-color: #0066cc; + color: #fff; + font-size: 18px; + font-weight: bold; + margin-top: var(--listMargin); + cursor: pointer; +} + +.pages { + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/lci/extra/mgmt/templates/features/log.html b/lci/extra/mgmt/templates/features/log.html new file mode 100644 index 0000000..0a0389e --- /dev/null +++ b/lci/extra/mgmt/templates/features/log.html @@ -0,0 +1,12 @@ + + + + + + Document + + + + log + + \ No newline at end of file diff --git a/lci/extra/mgmt/templates/features/module.html b/lci/extra/mgmt/templates/features/module.html new file mode 100644 index 0000000..e2696ca --- /dev/null +++ b/lci/extra/mgmt/templates/features/module.html @@ -0,0 +1,35 @@ + + + + + + + Document + + + + + +
+ + +
+ + + + + + + {% for item in modules %} + + + + + {% endfor %} + +
模块名操作
{{ item }} + +
+ + + \ No newline at end of file diff --git a/lci/extra/mgmt/templates/features/process.html b/lci/extra/mgmt/templates/features/process.html new file mode 100644 index 0000000..5468f9e --- /dev/null +++ b/lci/extra/mgmt/templates/features/process.html @@ -0,0 +1,199 @@ + + + + + + + Document + + + + + + + + + + + + + + + + + + {% for item in response %} + + + + + + + {% endfor %} + + +
PID状态命令操作
{{ item.pid }}{{ item.status }}{{ item.order }}
+
+ + + + + + + diff --git a/lci/extra/mgmt/templates/features/user.html b/lci/extra/mgmt/templates/features/user.html new file mode 100644 index 0000000..167420c --- /dev/null +++ b/lci/extra/mgmt/templates/features/user.html @@ -0,0 +1,162 @@ + + + + + + + Document + + + + + +
+ + +
+ + + + + + + + + + {% for item in response %} + + + + + + + {% endfor %} + +
用户ID用户名用户角色操作
{{ item.user_id}}{{ item.username }}{{ item.role }} + {% if current_role == "admin" %} + + {% if item.username != "admin" %} + + {% endif %} + {% endif %} + + {% if current_role != "admin" and current_user_id == item.user_id %} + + {% endif %} +
+ + + + + \ No newline at end of file diff --git a/lci/extra/mgmt/templates/index.html b/lci/extra/mgmt/templates/index.html new file mode 100644 index 0000000..59cfcd4 --- /dev/null +++ b/lci/extra/mgmt/templates/index.html @@ -0,0 +1,88 @@ + + + + + + + pmc management + + + + + +
+
Welcome to pmc management system
+
+
Name:
+ +
+
+
Password:
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/lci/extra/mgmt/templates/mgmt.html b/lci/extra/mgmt/templates/mgmt.html new file mode 100644 index 0000000..8132c34 --- /dev/null +++ b/lci/extra/mgmt/templates/mgmt.html @@ -0,0 +1,87 @@ + + + + + + + management page + + + + + + + +
+
当前用户: {{ username }}
+
当前角色: {{ role }}
+
+ +
+
+
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/lci/extra/phs/.gitignore b/lci/extra/phs/.gitignore new file mode 100644 index 0000000..0837616 --- /dev/null +++ b/lci/extra/phs/.gitignore @@ -0,0 +1,29 @@ +.git/ +.vscode/ +.idea/ +.cache/ + +# +init.csv + + +# cangjie +build/ +target/ +output/ +module-resolve.json +module-lock.json +cjpm.lock +.cjpm-history +*.macrocall +*.gcda +*.gcno + + +# 其他 +log +report +tmp + +# ide +.arts diff --git a/lci/extra/phs/LICENSE b/lci/extra/phs/LICENSE new file mode 100644 index 0000000..63b4b68 --- /dev/null +++ b/lci/extra/phs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/lci/extra/phs/README.md b/lci/extra/phs/README.md new file mode 100644 index 0000000..2e4db2a --- /dev/null +++ b/lci/extra/phs/README.md @@ -0,0 +1,24 @@ + +## 为什么要做一个这样的项目 + +当时想做一个树莓派机器人,是准备用python来写控制程序,可是当时python的多线程能力较弱, + +所以想到使用子系统,可是我没有VPN,无法安装ROS,所以就自己写了一个进程托管程序 + +## 使用方式 + +### 启动列表 +```bash +# 创建启动列表,每一行是一个进程 +touch init.csv +``` + +### 创建进程 +```bash +curl -X POST http://localhost:5200/api/v2/create -d '["ls"] +``` + +### 进程列表 +```bash +curl http://localhost:5200/api/v2/list +``` diff --git a/lci/extra/phs/_run.sh b/lci/extra/phs/_run.sh new file mode 100644 index 0000000..bb5c279 --- /dev/null +++ b/lci/extra/phs/_run.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd "$(dirname "$0")" || exit +#source ./.env/bin/activate +source /cangjie/envsetup.sh +exec cjpm run diff --git a/lci/extra/phs/cjpm.toml b/lci/extra/phs/cjpm.toml new file mode 100644 index 0000000..fd5453a --- /dev/null +++ b/lci/extra/phs/cjpm.toml @@ -0,0 +1,24 @@ +[package] + cjc-version = "1.0.0" + name = "phs" + description = "nothing here" + version = "1.0.0" + target-dir = "" + src-dir = "" + output-type = "executable" + compile-option = "" + override-compile-option = "" + link-option = "" + package-configuration = {} + +[dependencies] +[target.x86_64-unknown-linux-gnu] + [target.x86_64-unknown-linux-gnu.bin-dependencies] + path-option = ["/stdx/linux_x86_64_llvm/static/stdx"] +[target.aarch64-unknown-linux-gnu] + [target.aarch64-unknown-linux-gnu.bin-dependencies] + path-option = ["/stdx/linux_aarch64_llvm/static/stdx"] +[target.x86_64-w64-mingw32] # 系统架构和 OS 信息 + [target.x86_64-w64-mingw32.bin-dependencies] + path-option = ["C:\\stdx\\windows_x86_64_llvm\\dynamic\\stdx"] # stdx 路径根据实际情况配置 + diff --git a/lci/extra/phs/phs.service b/lci/extra/phs/phs.service new file mode 100644 index 0000000..c1b2956 --- /dev/null +++ b/lci/extra/phs/phs.service @@ -0,0 +1,19 @@ +[Unit] +Description=TxL.phs +After=network.target +Wants=network.target + +[Service] +User=root +Group=root +WorkingDirectory=/root/___TxL/TestSuits/phs +ExecStart=bash /root/___TxL/TestSuits/phs/_run.sh +Restart=always +RestartSec=10 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=mozart-python-host +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target diff --git a/lci/extra/phs/src/Th.cj b/lci/extra/phs/src/Th.cj new file mode 100644 index 0000000..511fc2a --- /dev/null +++ b/lci/extra/phs/src/Th.cj @@ -0,0 +1,312 @@ +package phs +import std.sync.*; + +/* + * Status 状态 + * + * FIXME: 在枚举中定义操作函数 + */ +enum Stat { + SHUT | STOP | START | RUNNING +} + + +/* + * Finite Status Machine 有限状态机 + * + * + * FIXME(TritiumQin): 1. 柯里化 + * + * FIXME(qing): 支持自定义状态 + */ +open class Fsm { + + + /* 状态枚举 */ + private var stat: Stat = STOP + + /* 线程安全 */ + private let mtx: Mutex = Mutex() + + /* + * 条件变量 + * + * FIXME: 我不知道这么写意义何在 + */ + private let condition: Condition = synchronized(mtx) { + mtx.condition() + } + + /* 辅助条件变量的布尔标志 */ + private var flag: Bool = true + + + /* ---------------------------------------------- */ + + /* + * 阻塞等待状态改变,可以传入一个谓词,以确定什么时候终止 + * + * 在该回调函数中返回true,则等待不会停止 + */ + public func wait(keep!: (Stat) -> Bool = {_ => false}) { + synchronized(mtx) { + flag = true + while(flag || keep(stat)) { + condition.wait() + } + } + } + + /* ---------------------------------------------- */ + + /* + * 对状态机进行某种操作,输入一个谓词。 + * + * 会激活堵塞线程 + */ + public func doc(p: (Stat) -> Stat): Stat { + synchronized(mtx) { + stat = p(stat) + flag= false // 试图唤醒阻塞的线程 + condition.notifyAll() + return stat + } + } + + /* + * 对状态机进行某种操作,输入一个谓词。 + * + * 不会激活堵塞线程 + */ + public func dop(p: (Stat) -> Stat): Stat { + synchronized(mtx) { + stat = p(stat) + return stat + } + } + + +} + + +/* Controllable Thread 可控制线程 + * + * FIXME(花间岛): + * 1. 使用async语法糖 + * 2. 封装iocp + */ +open class Th { + + /* 持有线程类型 */ + private var fut: Option> = None + + /* 持有状态机类型 */ + private let fsm: Fsm = Fsm() + + /*----------------------------------------------*/ + + /* 持有静止事件回调 */ + private var StopEvent: Option<() -> Unit> = None + + /* 持有唤醒事件回调 */ + private var WakeEvent: Option<() -> Unit> = None + + /* 持有循环事件回调 */ + private var LoopEvent: Option<() -> Unit> = None + + /* 持有清理事件回调 */ + private var ClearEvent: Option<() -> Unit> = None + + /* 持有线程主函数回调 */ + private var Main: Option<() -> Int64> = None // 线程主函数 + + /*----------------------------------------------*/ + + /* 使用谓词检查线程状态 */ + public func chkp(): Stat { + return fsm.dop({ stat => return stat }) + } + + /*----------------------------------------------*/ + + /* 进入关闭状态 */ + public func shut() { + fsm.doc({ _ => return SHUT }) + } + + /* 进入静止态 */ + public func stop() { + fsm.doc({ _ => return STOP }) + } + + /* 进入启动状态 */ + public func start() { + fsm.doc({ _ => return START }) + } + + /* 进入运行态 */ + public func run() { + fsm.doc({ _ => return RUNNING }) + } + + /*----------------------------------------------*/ + + /* 状态机是否处于静止态 */ + public func isStop() { + return match( chkp() ) { + case STOP => true + case _ => false + }} + + /* 状态机是否处于关闭态 */ + public func isShut() { + return match( chkp() ) { + case SHUT => true + case _ => false + }} + + /* 状态机是否处于开始状态 */ + public func isStart() { + return match( chkp() ) { + case START => true + case _ => false + }} + + /* 状态机是否处于运行态 */ + public func isRunning() { + return match( chkp() ) { + case RUNNING => true + case _ => false + }} + + /*----------------------------------------------*/ + + /* 扮演函数,即设置线程事件 */ + public func Act( + + /* 静止事件 */ + stop!: Option<() -> Unit> = None, + + /* 唤醒事件 */ + wake!: Option<() -> Unit> = None, + + /* 循环事件 */ + loop!: Option<() -> Unit> = None, + + /* 清理事件 */ + clear!:Option<() -> Unit> = None, + + + /* 线程主函数 */ + mainloop!: Option<() -> Int64>= None + + ) + + { + + /* 只有静止态才能进行设置 */ + if (!isStop()) { return; } + + /* ------------------------------------------------- */ + + /* 设置静止事件,确保退出了静止态 */ + StopEvent = stop ?? {=> fsm.wait(keep: {stat => + match(stat) { + case STOP => true /* 保持等待 */ + case _ => false + } + })} + + /* 唤醒事件,默认直接进入运行态 */ + WakeEvent = wake ?? {=> this.run()} + + /* 循环事件,默认进行延时 */ + LoopEvent = loop ?? {=> sleep(100 * Duration.millisecond)} + + /* 清理事件,默认什么都不做 */ + ClearEvent= clear?? {=> ;} + + /* ------------------------------------------------- */ + + /* 线程主函数 */ + Main = mainloop ?? {=> + + /* 回字形循环 外层 */ + while (!isShut()) { + this.inner() + } + + + return 0 + } + + /* ------------------------------------------------- */ + + } + + + /* ------------- 回字形循环 内层 -------------- */ + + private func inner() { + + /* 静止状态 */ + while(isStop()) { StopEvent.getOrThrow()() } + + /* 唤醒事件 */ + if (isStart()) { WakeEvent.getOrThrow()() } + + /* 运行状态 */ + while(isRunning()) { LoopEvent.getOrThrow()() } + + /* 清理事件 */ + ClearEvent.getOrThrow()() + + } + + /* --------------------------------------------- */ + + /* + * 创建资源,可控线程苏醒 + * + * 如果在调用中等待线程启动,可能会造成死锁 + * 想了想觉得也没有这个必要 + */ + public func wake() { + + + /* 存在微小的冲突可能,所以我不让它进入静止态 */ + this.start() + + + /* 创建卵 */ + fut = Some>(spawn { + Main.getOrThrow()() + }) + + + /* 等启动 + * + * 20251020(qing): 如果线程启动太快,该等待可能会导致死锁 + */ + //fsm.wait(keep: {stat => match(stat) { + // case START => true + // case _ => false + //}}) + } + + /* 等待线程结束 */ + public func wait() { + let f = fut.getOrThrow() + return f.get() + } + + /* + * 关闭线程,然后等待线程结束 + */ + public func shut_and_wait() { + shut() + wait() + } + + +} diff --git a/lci/extra/phs/src/main.cj b/lci/extra/phs/src/main.cj new file mode 100644 index 0000000..69431c6 --- /dev/null +++ b/lci/extra/phs/src/main.cj @@ -0,0 +1,211 @@ +package phs + +import std.io.*; +import std.fs.*; +import std.collection.*; +import std.sync.*; +import std.process.*; +import stdx.net.http.*; +import stdx.encoding.json.*; + +const ADDR = "127.0.0.1" /* 监听IP地址 */ +const PORT = 5200u16 /* 端口 UInt16类型 */ + + +class Ps { + private let ps = LinkedList() + private let mtx = Mutex() + + /* 创建进程 */ + public func create(arr: Array) { + let path = arr[0] + let args = arr[1..] + let subp: SubProcess = launch(path, args, + //stdIn: ProcessRedirect.Discard, /* WINDOWS中不支持输入重定向 */ + //stdOut: ProcessRedirect.Discard, /* 加了这行之后就在控制台看不到输出了 */ + //stdErr: ProcessRedirect.Discard + ) + this.clear() /* 清理进程 */ + synchronized(mtx) { + ps.addLast(subp) + return; + } + } + + /* 返回进程列表 */ + public func list() { + var msg = "" +println('1') + this.clear() /* 清理进程,有锁 */ +println('2') + synchronized(mtx) { +println('3') + for (p in ps) { + if (msg != "") { msg += "\n"; } + msg += '${p.pid.toString()}\t' + msg += '${p.command} ' + //for (i in p.arguments) { /* 加上这个会失败 */ + // msg += '${i} ' + //} + /* 启动时间 */ + msg += '\t${p.userTime.toString()}' + } +println('4') + } + return msg; + } + + /* 杀死已经存在的进程 */ + public func kill(pid: Int64) { + this.clear() /* 清理 */ + synchronized(mtx) { + for (p in ps) { + if (p.pid == pid) { + p.terminate(force: true) + return + } + } + } + } + + /* + * 进程自启动 + * + * FIXME: 支持注释 + */ + public func autoRun(csvpath: String) { + let file = File(csvpath, Read) + let bytes = readToEnd(file) + let ctt = String.fromUtf8(bytes) + let cmds = ctt.split("\n", removeEmpty: true) + for (cmd in cmds) { + try{ + let arr = cmd.split(" ", removeEmpty: true) + this.create(arr) + } catch (exp: Exception) { + ; + } + } + file.close() + } + + /* 清理已经死去的进程,默认不加锁 */ + private func clear() { + synchronized(mtx) { + let len = ps.size + for (i in (len-1)..=0 : -1) { + try { + let p = ps.nodeAt(i).getOrThrow() +println("start...") + p.value.wait(timeout: 1 * Duration.millisecond) /* 进程有没有退出 */ +println("end...") + ps.remove(p) + } + catch (exp: TimeoutException) { /* 如果进程没有结束,会运行到这里 */ + ; +println("skip...") + } + } + } + } + +} + + +/* + * 将JSON数组转为字符串的数组 + * + * Json对象的每个元素只能是字符串,否则会报错 + */ +func Jarr_to_arr(arr: JsonArray): Array { + let len = arr.size() // Json数组的长度 + let ret = Array(len, repeat: "") + for (i in 0..len) { + let option: Option = arr.get(i) + let jitem: JsonValue = option.getOrThrow() + let jstr: JsonString = jitem.asString() + let str: String = jstr.getValue() + ret[i] = str + } + + + return ret +} + +/* cj命令行参数不包含可执行文件名 */ +main(_: Array): Int64 { + + /* 进程组 */ + let ps = Ps() + ps.autoRun("./init.csv") + + + /* 创建http服务器 */ + let server: Server = ServerBuilder() + .addr(ADDR) /* 设置地址 */ + .port(PORT) /* 设置端口 */ + .build() + + /* 创建进程的HTTP方法 */ + server.distributor.register("/api/v2/create", { context => + if (context.request.method == "POST") { + let content = StringReader(context.request.body).readToEnd() /* 阅读请求体 */ + let array = JsonValue.fromStr(content).asArray() /* 解析json格式的请求体 */ + let net_args: Array = Jarr_to_arr(array) /* json数组转原生数组 */ + ps.create(net_args) + + /* ---- 准备响应 ---- */ + let Headers = HttpHeaders() + Headers.add("Content-Type", "text/html; charset=utf-8;") + context.responseBuilder.setHeaders(Headers) + context.responseBuilder.body('OK') + } + }) + + /* + * 输出进程列表的HTTP方法 + */ + server.distributor.register("/api/v2/list", { context => + if (context.request.method == "GET") { + +println('a') + /* 准备表单字符串 */ + let ret = ps.list() + +println('b') + /**** 准备响应 ****/ + let Headers = HttpHeaders() + Headers.add("Content-Type", "text/html; charset=utf-8;") + context.responseBuilder.setHeaders(Headers) + context.responseBuilder.body(ret) + +println('c') + } + }) + + /* 杀死一个已经存在的进程 */ + server.distributor.register("/api/v2/kill", { context => + if (context.request.method == "POST") { + let content = StringReader(context.request.body).readToEnd() /* 阅读请求体 */ + let target_pid = JsonValue.fromStr(content) + .asObject() + .get("pid") + .getOrThrow() + .asInt() + .getValue() + ps.kill(target_pid) + + /**** 准备响应 ****/ + let Headers = HttpHeaders() + Headers.add("Content-Type", "text/html; charset=utf-8;") + context.responseBuilder.setHeaders(Headers) + context.responseBuilder.body("done") + } + }) + + /* 服务器开始运行 */ + server.serve() + return 0 + +} + diff --git a/lci/extra/phs/test/crt.py b/lci/extra/phs/test/crt.py new file mode 100644 index 0000000..27c8f1b --- /dev/null +++ b/lci/extra/phs/test/crt.py @@ -0,0 +1,66 @@ +import requests +import json + + +def create_process(cmd_list, url="http://localhost:5200/api/v2/create"): + """创建任意进程 + + Args: + cmd_list (list): 命令和参数列表,例如 ["timeout", "/t", "1000"] + url (str, optional): 请求URL,默认为 "http://localhost:5200/api/v2/create" + + Returns: + requests.Response: 响应对象 + """ + try: + # 发送POST请求 + response = requests.post(url, data=json.dumps(cmd_list), timeout=30) + + # 返回响应对象供进一步处理 + return response + except requests.exceptions.RequestException as e: + print(f"创建进程失败: {e}") + return None + + +def create_process_with_output(cmd_list, url="http://localhost:5200/api/v2/create"): + """创建任意进程并打印输出 + + Args: + cmd_list (list): 命令和参数列表,例如 ["timeout", "/t", "1000"] + url (str, optional): 请求URL,默认为 "http://localhost:5200/api/v2/create" + + Returns: + dict or str: 响应内容,JSON解析后的字典或原始文本 + """ + response = create_process(cmd_list, url) + + if response is None: + return None + + # 打印响应状态码 + print(f"响应状态码: {response.status_code}") + + # 尝试解析响应内容为JSON格式并打印 + try: + response_json = response.json() + print(f"响应内容: {json.dumps(response_json, indent=2, ensure_ascii=False)}") + return response_json + except json.JSONDecodeError: + print(f"响应内容(非JSON格式): {response.text}") + return response.text + + +if __name__ == "__main__": + # 示例用法 + # 创建一个timeout进程,延迟1000毫秒 + cmd = ["timeout", "/t", "1000"] + + # 使用默认URL + print("使用默认URL:") + create_process_with_output(cmd) + + # 示例:使用自定义URL(注释掉,可根据需要取消注释) + # print("\n使用自定义URL:") + # custom_url = "http://localhost:5200/api/v2/create" + # create_process_with_output(cmd, custom_url) diff --git a/lci/extra/phs/test/test_process_create_win.py b/lci/extra/phs/test/test_process_create_win.py new file mode 100644 index 0000000..13343bb --- /dev/null +++ b/lci/extra/phs/test/test_process_create_win.py @@ -0,0 +1,30 @@ +import requests +import json + +def test_create_process(): + """测试访问phs web服务的create接口""" + # 设置请求URL + url = "http://localhost:5200/api/v2/create" + + # 设置请求数据 + data = ["timeout", "/t", "1000"] + + # 发送POST请求 + response = requests.post(url, data=json.dumps(data)) + + # 打印响应状态码 + print(f"响应状态码: {response.status_code}") + + # 尝试解析响应内容为JSON格式并打印 + try: + response_json = response.json() + print(f"响应内容: {json.dumps(response_json, indent=2, ensure_ascii=False)}") + except json.JSONDecodeError: + print(f"响应内容(非JSON格式): {response.text}") + + # 返回响应对象供进一步处理 + return response + +if __name__ == "__main__": + # 执行测试函数 + test_create_process() \ No newline at end of file diff --git a/lci/extra/phs/test/test_process_kill_win.py b/lci/extra/phs/test/test_process_kill_win.py new file mode 100644 index 0000000..883f3a9 --- /dev/null +++ b/lci/extra/phs/test/test_process_kill_win.py @@ -0,0 +1,44 @@ +import requests +import json +import argparse + + +def test_kill_process(pid): + """测试访问phs web服务的kill接口""" + # 设置请求URL + url = "http://localhost:5200/api/v2/kill" + + # 构造请求消息体 + payload = { + "pid": pid + } + + # 发送POST请求 + response = requests.post(url, json=payload) + + # 打印响应状态码 + print(f"响应状态码: {response.status_code}") + + # 尝试解析响应内容为JSON格式并打印 + try: + response_json = response.json() + print(f"响应内容: {json.dumps(response_json, indent=2, ensure_ascii=False)}") + except json.JSONDecodeError: + print(f"响应内容(非JSON格式): {response.text}") + + # 返回响应对象供进一步处理 + return response + + +if __name__ == "__main__": + # 创建命令行参数解析器 + parser = argparse.ArgumentParser(description="测试访问phs web服务的kill接口") + + # 添加pid参数 + parser.add_argument("pid", type=int, help="要杀死的进程ID") + + # 解析命令行参数 + args = parser.parse_args() + + # 执行测试函数,传入命令行参数中的pid + test_kill_process(args.pid) diff --git a/lci/extra/phs/test/test_process_list_win.py b/lci/extra/phs/test/test_process_list_win.py new file mode 100644 index 0000000..45edcdf --- /dev/null +++ b/lci/extra/phs/test/test_process_list_win.py @@ -0,0 +1,27 @@ +import requests +import json + +def test_create_process(): + """测试访问phs web服务的create接口""" + # 设置请求URL + url = "http://localhost:5200/api/v2/list" + + # 发送POST请求 + response = requests.get(url) + + # 打印响应状态码 + print(f"响应状态码: {response.status_code}") + + # 尝试解析响应内容为JSON格式并打印 + try: + response_json = response.json() + print(f"响应内容: {json.dumps(response_json, indent=2, ensure_ascii=False)}") + except json.JSONDecodeError: + print(f"响应内容(非JSON格式): {response.text}") + + # 返回响应对象供进一步处理 + return response + +if __name__ == "__main__": + # 执行测试函数 + test_create_process() \ No newline at end of file diff --git a/lci/extra/qemu_run_appimage/LICENSE b/lci/extra/qemu_run_appimage/LICENSE new file mode 100644 index 0000000..63f5ec4 --- /dev/null +++ b/lci/extra/qemu_run_appimage/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 gopsc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lci/extra/qemu_run_appimage/README.md b/lci/extra/qemu_run_appimage/README.md new file mode 100644 index 0000000..0be57e2 --- /dev/null +++ b/lci/extra/qemu_run_appimage/README.md @@ -0,0 +1,91 @@ +## qemu_run_appimage是什么? + +这个项目源于我想用qemu-user运行AppImage的想法。 + +qemu-user是一款支持多种架构的能够虚拟化运行单个程序的虚拟机软件,而AppImage是一款能够打包出单文件应用的工具。 + +## 跨架构运行的障碍是什么? + +Linux系统支持许多架构,这些架构的机器码、汇编语言都是有差异的,所以不同架构的系统在一般情况下是无法混合使用的,qemu虚拟机能够解决这个痛点,但是这需要一些技巧。 + +通常而言,一个程序除了在编译阶段静态链接了许多库,出于一些原因的考量,它也会在运行时动态地加载一些动态库文件。 + +使用qemu-user跨架构运行,常常就会出现单个文件能够执行,但是动态链接库找不到的问题。 + +## AppImage起到一个什么作用? + +AppImage能够将许多文件打包成一个二进制程序,你双击运行它就能够直接执行,因为关键的动态连接库都包含在里面了,通过一些技术手段让程序能够访问到。 + +所以我们能够将程序以及动态库打包成单个可执行程序,就有了能够虚拟化运行该程序的先决条件。 + +## 如何使用qemu-user运行AppImage + +通常而言,AppImage里面会有一个叫AppRun的启动脚本,运行AppImage会使用shell解释器启动它,然后该脚本配置好环境,再由它启动目标程序。 + +但是qemu-user虚拟出的进程往往无法在宿主机系统中找到与它架构相对应的shell,所以就无法执行常规方式打包的AppImage文件。 + +那我通过与deepseek的深度交流,最终解决了这个问题。 + +我的思路有两个,一是打包AppImage时将AppRun编译成二进制文件,这个办法我经过了验证,是可行的。 + +还有一个办法是再打包一个shell解释器进去,这个我还没有尝试、 + +在运行时,我采用qemu-user提取出AppImage中的所有的文件,并且放在一个临时文件夹,然后在设置引用环境为该文件夹的方式下通过qemu-user执行其中的AppRun,应该就能够启动程序。 + +目前已经尝试了在树莓派上运行x86_64架构的一系列工具,包含ls命令、bash解释器、python3解释器,都能够成功,接下来我还会对网络等功能进行测试。 + +## 这个方法有哪些缺点? + +我像一些朋友展示了这个方式,能够运行测试用例,但是他们说qemu的速度很慢,我了解了一下,qemu-user的性能损耗可能在2倍以内,一般的程序似乎还是可以接受,qemu虚拟机完整情况下的确较慢,性能损耗能够达到8~10倍。 + +所以一些开发者对常见架构进行了深度优化,成果比如box64(Arm64平台运行X86程序)、LATX(LA64平台运行x86程序),这些程序虽然支持的架构没有qemu-user多,在一些平台上可能也需要单独配置一些东西,但是在它们支持的架构下拥有更好的性能,在这里也推荐大家使用。 + +## 我的一些别的想法 + +既然qemu-user能够进行跨架构的实时翻译,那我们可能也能做到将一个二进制文件直接再编译成其他架构的二进制文件?这是我的一个想法。 + +## 项目逻辑与使用方法 + +项目中的auto-appimage1.sh这是一个打包脚本,能够在要运行的目标架构上打包出包含所有链接库的AppImage文件,且其中的入口程序AppRun是二进制的。 + +该脚本的使用方式如下: + +```bash auto-appimage1.sh ``` + +目前这个脚本仅能打包x86的程序,如果要在别的平台打包AppImage,可能需要进行调整。 + +qemu_run2.sh是在宿主机上的启动脚本,其中使用了qemu-user-static系列工具创建虚拟环境,static后缀的意思是这个工具是静态编译的,如果你的架构上没有这个软件,请安装它的非静态版本qemu-user,然后将其中的qemu-*-static都改为qemu-* + +通过一下方式使用它: + +```bash qemu_run2.sh ``` + +ls.AppImage是x86_64架构下的ls命令打包,是测试用例。 + +该项目的依赖: + +--------------- + +打包机器依赖项: + +appimagetool需要在github下载制品,并且添加可执行权限放到环境变量里。 + +```apt install fuse libfuse2``` + +--------------- + +宿主机依赖: + +```apt install qemu-user-static``` + +或者 + +```apt install aemu-user``` + + ———————————————— + +版权声明:本文为CSDN博主「gapcuda」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 +原文链接:https://blog.csdn.net/gapcuda/article/details/156559857 + + + (这是我自己的csdn账号) \ No newline at end of file diff --git a/lci/extra/qemu_run_appimage/auto-appimage1.sh b/lci/extra/qemu_run_appimage/auto-appimage1.sh new file mode 100644 index 0000000..f1c9585 --- /dev/null +++ b/lci/extra/qemu_run_appimage/auto-appimage1.sh @@ -0,0 +1,686 @@ +#!/bin/bash +# appimage-packager-rofs-fixed.sh +# 已添加QEMU兼容性支持 + +set -e + +# 彩色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 创建图标 +create_icon() { + local icon_path="$1" + mkdir -p "$(dirname "$icon_path")" + + echo 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' | \ + base64 -d > "$icon_path" 2>/dev/null || \ + echo -ne '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xf8\x0f\x00\x00\x01\x00\x01\x05\x01\x00\x00\x00\x00IEND\xaeB`\x82' > "$icon_path" +} + +# 创建简单二进制AppRun(修复版) +create_simple_binary_apprun() { + local app_dir="$1" + local app_name="$2" + local orig_name="$3" + + echo "创建简单二进制AppRun..." + + # 先回退到脚本版本 + cat > "$app_dir/AppRun" << 'EOF' +#!/bin/sh +# 简单启动脚本(兼容性最好) +HERE="$(dirname "$(readlink -f "$0")")" +APP_NAME="$(basename "$0" .AppImage)" + +# 设置环境 +export PATH="$HERE/usr/bin:$PATH" +export LD_LIBRARY_PATH="$HERE/usr/lib:$LD_LIBRARY_PATH" + +# 检测架构 +if [ "$(uname -m)" = "x86_64" ]; then + # x86系统直接运行 + exec "$HERE/usr/bin/$APP_NAME" "$@" +else + # ARM系统,通过QEMU运行 + # 确保有lib64目录 + if [ ! -d "$HERE/lib64" ] && [ -f "$HERE/usr/lib/ld-linux-x86-64.so.2" ]; then + mkdir -p "$HERE/lib64" + cp "$HERE/usr/lib/ld-linux-x86-64.so.2" "$HERE/lib64/" 2>/dev/null || true + fi + + # 使用QEMU运行 + exec qemu-x86_64-static -L "$HERE" "$HERE/usr/bin/$APP_NAME" "$@" +fi +EOF + + chmod +x "$app_dir/AppRun" + echo -e "${GREEN}✓ 创建脚本版AppRun${NC}" +} + +# 创建二进制AppRun函数(修复版) +create_binary_apprun() { + local app_dir="$1" + local app_name="$2" + local orig_name="$3" + + echo -e "${YELLOW}创建二进制AppRun...${NC}" + + # 创建修复的C源代码 + cat > /tmp/apprun_fixed.c << 'EOF' +#include +#include +#include +#include +#include +#include +#include + +// 获取程序所在目录 +static char* get_program_dir() { + static char path[4096]; + ssize_t len = readlink("/proc/self/exe", path, sizeof(path)-1); + if (len == -1) { + return NULL; + } + path[len] = '\0'; + + // 找到最后一个'/' + char* last_slash = strrchr(path, '/'); + if (last_slash) { + *last_slash = '\0'; + } + return path; +} + +// 检查文件是否存在 +static int file_exists(const char* path) { + struct stat st; + return stat(path, &st) == 0 && S_ISREG(st.st_mode); +} + +// 执行程序 +static void execute_program(const char* dir, const char* prog_name, char* const argv[]) { + char prog_path[4096]; + snprintf(prog_path, sizeof(prog_path), "%s/usr/bin/%s", dir, prog_name); + + if (!file_exists(prog_path)) { + // 尝试查找任何可执行文件 + fprintf(stderr, "错误: 未找到可执行文件 %s\n", prog_path); + exit(1); + } + + // 设置环境变量 + setenv("HERE", dir, 1); + + char ld_path[4096]; + snprintf(ld_path, sizeof(ld_path), "%s/usr/lib", dir); + setenv("LD_LIBRARY_PATH", ld_path, 1); + + char path[4096]; + const char* old_path = getenv("PATH"); + if (old_path) { + snprintf(path, sizeof(path), "%s/usr/bin:%s", dir, old_path); + } else { + snprintf(path, sizeof(path), "%s/usr/bin", dir); + } + setenv("PATH", path, 1); + + // 执行程序 + execv(prog_path, argv); + // 如果execv失败 + fprintf(stderr, "无法执行程序: %s\n", prog_path); + exit(1); +} + +int main(int argc, char* argv[]) { + // 获取程序所在目录 + char* dir = get_program_dir(); + if (!dir) { + fprintf(stderr, "无法获取程序目录\n"); + return 1; + } + + // 从AppImage文件名获取程序名 + char* prog_name = "program"; + + // 尝试从argv[0]获取 + if (argc > 0 && argv[0]) { + char* app_name = argv[0]; + char* last_slash = strrchr(app_name, '/'); + if (last_slash) { + app_name = last_slash + 1; + } + + // 移除.AppImage后缀 + char* dot_appimage = strstr(app_name, ".AppImage"); + if (dot_appimage) { + *dot_appimage = '\0'; + prog_name = app_name; + } + } + + // 准备新参数数组 + char** new_argv = malloc((argc + 1) * sizeof(char*)); + if (!new_argv) { + fprintf(stderr, "内存分配失败\n"); + return 1; + } + + new_argv[0] = prog_name; + for (int i = 1; i < argc; i++) { + new_argv[i] = argv[i]; + } + new_argv[argc] = NULL; + + // 执行程序 + execute_program(dir, prog_name, new_argv); + + free(new_argv); + return 0; +} +EOF + + # 尝试编译为x86_64二进制(静态链接) + echo "编译x86_64二进制..." + + # 检查是否有交叉编译器 + if command -v x86_64-linux-gnu-gcc >/dev/null 2>&1; then + echo "使用x86_64-linux-gnu-gcc编译..." + x86_64-linux-gnu-gcc -static -Os -o "$app_dir/AppRun" /tmp/apprun_fixed.c 2>&1 | grep -v "warning" || true + elif command -v gcc >/dev/null 2>&1; then + echo "使用gcc交叉编译..." + gcc -target x86_64-linux-gnu -static -Os -o "$app_dir/AppRun" /tmp/apprun_fixed.c 2>&1 | grep -v "warning" || true + else + echo "未找到编译器" + fi + + # 检查编译是否成功 + if [[ -f "$app_dir/AppRun" ]] && [[ -x "$app_dir/AppRun" ]]; then + # 验证是x86_64二进制 + if file "$app_dir/AppRun" | grep -q "x86-64"; then + chmod +x "$app_dir/AppRun" + echo -e "${GREEN}✓ 二进制AppRun创建成功${NC}" + file "$app_dir/AppRun" + return 0 + else + echo "编译出的不是x86-64二进制" + rm -f "$app_dir/AppRun" + fi + fi + + # 编译失败,使用简单脚本 + echo "二进制编译失败,使用脚本版本" + create_simple_binary_apprun "$app_dir" "$app_name" "$orig_name" + + rm -f /tmp/apprun_fixed.c +} + +# 创建简单二进制AppRun +create_simple_binary_apprun() { + local app_dir="$1" + local app_name="$2" + local orig_name="$3" + + # 创建极简的汇编二进制 + cat > /tmp/minimal.S << 'EOF' +.section .note.GNU-stack,"",@progbits +.section .text +.globl _start +_start: + # execve("./usr/bin/program", argv, envp) + mov $59, %rax # syscall: execve + + # 构建路径字符串 + lea path(%rip), %rdi # arg1: filename + + # 构建参数数组 ["./usr/bin/program", NULL] + lea argv(%rip), %rsi # arg2: argv + + # 环境变量 + xor %rdx, %rdx # arg3: envp = NULL + + syscall + + # 如果失败,退出 + mov $60, %rax # syscall: exit + mov $1, %rdi # status = 1 + syscall + +path: + .ascii "./usr/bin/program\0" + +argv: + .quad path + .quad 0 +EOF + + # 汇编并链接 + as --64 -o /tmp/minimal.o /tmp/minimal.S + ld -m elf_x86_64 -s -o "$app_dir/AppRun" /tmp/minimal.o + + if [[ -f "$app_dir/AppRun" ]]; then + chmod +x "$app_dir/AppRun" + echo -e "${GREEN}✓ 极简二进制AppRun已创建${NC}" + else + echo -e "${RED}错误: 无法创建二进制AppRun${NC}" + # 回退到脚本 + create_script_apprun "$app_dir" "$app_name" "$orig_name" + fi + + rm -f /tmp/minimal.S /tmp/minimal.o +} + +# 使用busybox作为AppRun +use_busybox_as_apprun() { + local app_dir="$1" + + echo "使用busybox作为AppRun..." + + # 查找或下载x86_64的busybox + if [[ -f /usr/bin/busybox ]] && file /usr/bin/busybox | grep -q "x86-64"; then + cp /usr/bin/busybox "$app_dir/AppRun" + elif [[ -f /bin/busybox ]] && file /bin/busybox | grep -q "x86-64"; then + cp /bin/busybox "$app_dir/AppRun" + else + # 尝试下载静态版busybox + echo "下载静态busybox..." + wget -q -O "$app_dir/AppRun" "https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox" 2>/dev/null || { + echo "下载失败,使用备用方案" + return 1 + } + fi + + if [[ -f "$app_dir/AppRun" ]]; then + chmod +x "$app_dir/AppRun" + # 创建busybox的symlink脚本 + cat > "$app_dir/.busybox-run" << 'EOF' +#!/bin/sh +# busybox wrapper +HERE=$(dirname "$0") +export PATH="$HERE/usr/bin:$PATH" +export LD_LIBRARY_PATH="$HERE/usr/lib:$LD_LIBRARY_PATH" +exec "$HERE/AppRun" "$HERE/usr/bin/$(basename "$0" .AppImage)" "$@" +EOF + chmod +x "$app_dir/.busybox-run" + echo -e "${GREEN}✓ 使用busybox作为AppRun${NC}" + return 0 + fi + return 1 +} + +# 收集所有动态库依赖 +collect_all_dependencies() { + local executable="$1" + local lib_dir="$2" + + echo -e "${YELLOW}收集动态库依赖...${NC}" + + if ! ldd "$executable" 2>/dev/null | grep -q "=>"; then + echo "程序是静态链接,无需库文件" + return 0 + fi + + mkdir -p "$lib_dir" + local processed_libs=() + + collect_libs_recursive() { + local target="$1" + + ldd "$target" 2>/dev/null | grep "=>" | awk '{print $3}' | while read -r lib; do + if [[ -f "$lib" ]]; then + local libname=$(basename "$lib") + + if [[ ! " ${processed_libs[@]} " =~ " ${libname} " ]]; then + processed_libs+=("$libname") + + cp "$lib" "$lib_dir/" 2>/dev/null + if [[ $? -eq 0 ]]; then + echo " ✅ $libname" + collect_libs_recursive "$lib" + fi + fi + fi + done + } + + collect_libs_recursive "$executable" + + # 添加基础库 + local common_libs=( + "ld-linux-x86-64.so.2" + "libc.so.6" "libm.so.6" "libpthread.so.0" + "libdl.so.2" "librt.so.1" "libgcc_s.so.1" + "libstdc++.so.6" + ) + + for lib in "${common_libs[@]}"; do + find /usr/lib /lib /lib64 -name "$lib" -type f 2>/dev/null | head -1 | while read -r libpath; do + if [[ -f "$libpath" ]] && [[ ! -f "$lib_dir/$(basename "$libpath")" ]]; then + cp "$libpath" "$lib_dir/" 2>/dev/null && echo " ✅ 基础: $(basename "$libpath")" + fi + done + done + + local lib_count=$(ls -1 "$lib_dir"/*.so* 2>/dev/null | wc -l) + echo -e "${GREEN}✓ 添加了 $lib_count 个库${NC}" +} + +# 设置QEMU目录结构 +setup_qemu_structure() { + local app_dir="$1" + + echo -e "${YELLOW}设置QEMU目录结构...${NC}" + + # 创建QEMU需要的目录结构 + mkdir -p "$app_dir/lib64" + mkdir -p "$app_dir/usr/gnemul/qemu-x86_64" + + # 复制库到QEMU期望的位置 + if [[ -d "$app_dir/usr/lib" ]]; then + echo "复制库文件到QEMU目录..." + # 复制所有库 + cp -r "$app_dir/usr/lib/"* "$app_dir/usr/gnemul/qemu-x86_64/" 2>/dev/null || true + + # 特别处理ld-linux动态链接器 + if [[ -f "$app_dir/usr/lib/ld-linux-x86-64.so.2" ]]; then + echo "设置动态链接器..." + # 在lib64创建链接(QEMU查找的位置) + ln -sf ../usr/lib/ld-linux-x86-64.so.2 "$app_dir/lib64/ld-linux-x86-64.so.2" 2>/dev/null || true + # 同时也复制一份到lib64确保可用 + cp "$app_dir/usr/lib/ld-linux-x86-64.so.2" "$app_dir/lib64/" 2>/dev/null || true + fi + + echo -e "${GREEN}✓ QEMU目录结构已创建${NC}" + else + echo -e "${YELLOW}⚠ 未找到库目录,跳过QEMU结构设置${NC}" + fi +} + +# 查找并复制数据文件 +find_and_copy_data() { + local executable="$1" + local app_name="$2" + local data_dir="$3" + + echo -e "${YELLOW}查找数据文件...${NC}" + + local prog_dir=$(dirname "$(realpath "$executable")") + local patterns=("*.pem" "*.key" "*.crt" "*.cfg" "*.conf" "*.ini" "*.json" "*.xml") + + mkdir -p "$data_dir" + local count=0 + + # 首先查找程序目录 + for pattern in "${patterns[@]}"; do + find "$prog_dir" -maxdepth 2 -type f -name "$pattern" 2>/dev/null | while read -r file; do + if [[ "$file" != "$executable" ]]; then + cp "$file" "$data_dir/" 2>/dev/null && { + echo " ✅ $(basename "$file")" + count=$((count + 1)) + } + fi + done + done + + # 如果没有找到,尝试复制所有非可执行文件 + if [[ $count -eq 0 ]]; then + find "$prog_dir" -maxdepth 1 -type f ! -name "$(basename "$executable")" ! -name "*.AppImage" | while read -r file; do + cp "$file" "$data_dir/" 2>/dev/null && echo " ✅ $(basename "$file")" + done + fi + + # 如果没有数据文件,创建示例配置 + if [[ $count -eq 0 ]] && [[ ! -f "$data_dir/config.ini" ]]; then + cat > "$data_dir/config.ini" < [输出名称]" + echo " $0 -d <可执行文件> [输出名称] (包含数据文件)" + echo "" + echo "示例:" + echo " $0 ./myapp" + echo " $0 -d ./myapp" + exit 1 + fi + + # 检查是否包含数据文件 + INCLUDE_DATA=false + if [[ "$1" == "-d" ]] || [[ "$1" == "--data" ]]; then + INCLUDE_DATA=true + shift + fi + + echo -e "${BLUE}════════════════════════════════════════${NC}" + echo -e "${BLUE} AppImage 打包工具 ${NC}" + echo -e "${BLUE} (QEMU兼容版) ${NC}" + echo -e "${BLUE}════════════════════════════════════════${NC}" + + EXECUTABLE="$1" + APP_NAME="${2:-$(basename "$EXECUTABLE")}" + + [[ ! -f "$EXECUTABLE" ]] && { echo -e "${RED}错误: 文件不存在${NC}"; exit 1; } + [[ ! -x "$EXECUTABLE" ]] && chmod +x "$EXECUTABLE" + + echo "程序: $APP_NAME" + echo "原始: $(basename "$EXECUTABLE")" + + # 清理 + rm -rf "AppDir" "${APP_NAME}.AppImage" + + # =========== 步骤1: 创建目录 =========== + echo -e "\n${YELLOW}[1/7] 创建目录...${NC}" + mkdir -p AppDir/usr/bin + mkdir -p AppDir/usr/lib + + ORIG_NAME=$(basename "$EXECUTABLE") + + # =========== 步骤2: 复制程序 =========== + echo -e "\n${YELLOW}[2/7] 复制程序...${NC}" + cp "$EXECUTABLE" "AppDir/usr/bin/$ORIG_NAME" + chmod +x "AppDir/usr/bin/$ORIG_NAME" + echo -e "${GREEN}✓ 程序: $ORIG_NAME${NC}" + + # =========== 步骤3: 收集依赖 =========== + collect_all_dependencies "$EXECUTABLE" "AppDir/usr/lib" + + # =========== 步骤4: 设置QEMU目录结构 =========== + setup_qemu_structure "AppDir" + + # =========== 步骤5: 处理数据文件 =========== + if $INCLUDE_DATA; then + echo -e "\n${YELLOW}[5/7] 处理数据文件...${NC}" + find_and_copy_data "$EXECUTABLE" "$APP_NAME" "AppDir/usr/share/$APP_NAME" + fi + + # =========== 步骤6: 创建图标 =========== + echo -e "\n${YELLOW}[6/7] 创建图标...${NC}" + create_icon "AppDir/$APP_NAME.png" + cp "AppDir/$APP_NAME.png" "AppDir/.DirIcon" + echo -e "${GREEN}✓ 图标已创建${NC}" + +# =========== 步骤7: 创建二进制 AppRun =========== +echo -e "\n${YELLOW}[7/7] 创建二进制AppRun...${NC}" + +# 首先尝试创建C语言版本 +#use_busybox_as_apprun "AppDir" "$APP_NAME" "$ORIG_NAME" +create_binary_apprun "AppDir" "$APP_NAME" "$ORIG_NAME" + +# 如果失败,使用备选方案 +if [[ ! -f "AppDir/AppRun" ]] || [[ ! -x "AppDir/AppRun" ]]; then + echo "二进制创建失败,使用脚本版本..." + cat > "AppDir/AppRun" <<'EOF' +#!/bin/sh +# 兼容性脚本(当二进制不可用时) +HERE=$(dirname "$(readlink -f "$0")") +APP_NAME=$(basename "$0" .AppImage) + +# 对于QEMU:直接运行内部程序 +if [ "$(uname -m)" != "x86_64" ]; then + # ARM系统,通过QEMU运行 + exec qemu-x86_64-static -L "$HERE" "$HERE/usr/bin/$APP_NAME" "$@" +else + # x86系统,直接运行 + exec "$HERE/usr/bin/$APP_NAME" "$@" +fi +EOF + chmod +x AppDir/AppRun + echo -e "${YELLOW}⚠ 使用脚本版AppRun(QEMU兼容性可能有限)${NC}" +fi + + + # =========== 步骤8: 创建桌面文件 =========== + echo -e "\n${YELLOW}[8/7] 创建桌面文件...${NC}" + mkdir -p AppDir/usr/share/applications + cat > "AppDir/usr/share/applications/$APP_NAME.desktop" <&1 | grep -v "gpg2" | grep -v "Warning"; then + echo -e "${GREEN}✅ 打包成功!${NC}" + else + appimagetool --no-appstream --no-fuse AppDir "$OUTPUT_FILE" 2>&1 | grep -v "gpg2" | grep -v "Warning" + echo -e "${GREEN}✅ 打包成功!${NC}" + fi + + # =========== 测试 =========== + echo -e "\n${BLUE}════════════════════════════════════════${NC}" + echo -e "${GREEN}测试运行...${NC}" + + chmod +x "$OUTPUT_FILE" + + # 创建测试数据目录 + TEST_DIR="$HOME/.local/share/$APP_NAME-test" + rm -rf "$TEST_DIR" + mkdir -p "$TEST_DIR"/{config,data,logs,cache} + + # 复制可能的配置文件 + if [ -d "AppDir/usr/share/$APP_NAME" ]; then + cp -r AppDir/usr/share/$APP_NAME/* "$TEST_DIR/config/" 2>/dev/null || true + fi + + echo "测试命令:" + echo " 原生运行: ./\"$OUTPUT_FILE\" --help" + echo " QEMU运行: qemu-x86_64-static -L . ./\"$OUTPUT_FILE\" --help" + echo " 调试模式: DEBUG=1 ./\"$OUTPUT_FILE\" --help" + echo "----------------------------------------" + + # 原生测试运行 + export DEBUG=1 + if timeout 5s ./"$OUTPUT_FILE" --help 2>&1 | head -20; then + echo -e "${GREEN}✅ 原生运行正常${NC}" + elif timeout 5s ./"$OUTPUT_FILE" -h 2>&1 | head -20; then + echo -e "${GREEN}✅ 原生运行正常${NC}" + else + echo -e "${YELLOW}⚠ 原生运行可能需要特定参数${NC}" + fi + unset DEBUG + + # QEMU兼容性测试 + echo -e "\n${YELLOW}QEMU兼容性测试...${NC}" + if command -v qemu-x86_64-static >/dev/null 2>&1; then + echo "测试命令: qemu-x86_64-static -L . ./\"$OUTPUT_FILE\" --version" + if timeout 5s qemu-x86_64-static -L . ./"$OUTPUT_FILE" --version 2>&1 | head -5; then + echo -e "${GREEN}✅ QEMU运行成功${NC}" + elif timeout 5s qemu-x86_64-static -L . ./"$OUTPUT_FILE" -v 2>&1 | head -5; then + echo -e "${GREEN}✅ QEMU运行成功${NC}" + elif timeout 5s qemu-x86_64-static -L . ./"$OUTPUT_FILE" 2>&1 | head -5; then + echo -e "${GREEN}✅ QEMU运行成功${NC}" + else + echo -e "${YELLOW}⚠ QEMU测试需要特定参数${NC}" + echo "尝试: qemu-x86_64-static -L . ./\"$OUTPUT_FILE\" --help" + fi + else + echo "qemu-x86_64-static 未安装,跳过QEMU测试" + echo "安装命令: sudo apt install qemu-user-static" + fi + + # 最终信息 + echo -e "\n${BLUE}════════════════════════════════════════${NC}" + echo -e "${GREEN} 完成! ${NC}" + echo -e "${BLUE}════════════════════════════════════════${NC}" + echo "" + echo "📦 输出文件: $(realpath "$OUTPUT_FILE")" + echo "📏 大小: $(du -h "$OUTPUT_FILE" | cut -f1)" + echo "" + echo "🚀 使用方法:" + echo " 在x86_64系统: ./\"$OUTPUT_FILE\"" + echo " 在ARM系统: qemu-x86_64-static -L . ./\"$OUTPUT_FILE\"" + echo "" + echo "📁 用户数据目录:" + echo " $HOME/.local/share/$APP_NAME/" + echo "" + echo "🔧 调试模式:" + echo " DEBUG=1 ./\"$OUTPUT_FILE\" [参数]" + echo "" + echo "🔄 QEMU运行助手脚本:" + cat > "$(dirname "$OUTPUT_FILE")/run-with-qemu.sh" <<'EOF2' +#!/bin/bash +# QEMU运行助手 +APP="$1" +shift +qemu-x86_64-static -L . "$APP" "$@" +EOF2 + chmod +x "$(dirname "$OUTPUT_FILE")/run-with-qemu.sh" + echo " 已创建: $(dirname "$OUTPUT_FILE")/run-with-qemu.sh" + echo " 使用: ./run-with-qemu.sh \"$OUTPUT_FILE\" [参数]" + + # 清理 + rm -rf AppDir +} + +# 运行 +main "$@" diff --git a/lci/extra/qemu_run_appimage/ls.AppImage b/lci/extra/qemu_run_appimage/ls.AppImage new file mode 100644 index 0000000..0453892 Binary files /dev/null and b/lci/extra/qemu_run_appimage/ls.AppImage differ diff --git a/lci/extra/qemu_run_appimage/pmcd.AppImage b/lci/extra/qemu_run_appimage/pmcd.AppImage new file mode 100644 index 0000000..1d4e056 Binary files /dev/null and b/lci/extra/qemu_run_appimage/pmcd.AppImage differ diff --git a/lci/extra/qemu_run_appimage/qemu_run2.sh b/lci/extra/qemu_run_appimage/qemu_run2.sh new file mode 100644 index 0000000..555a3a5 --- /dev/null +++ b/lci/extra/qemu_run_appimage/qemu_run2.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# qemu_run_proper.sh + +APPIMAGE="$1" +shift + +if [[ ! -f "$APPIMAGE" ]]; then + echo "错误: 文件不存在: $APPIMAGE" >&2 + exit 1 +fi + +echo "=== 运行AppImage ===" +echo "文件: $(basename "$APPIMAGE")" + +# 创建临时目录 +TEMP_DIR=$(mktemp -d) +cp ${APPIMAGE} ${TEMP_DIR} +cd "$TEMP_DIR" + +# 提取AppImage +echo "提取AppImage..." +qemu-x86_64 "$APPIMAGE" --appimage-extract 2>/dev/null || { + echo "提取失败" >&2 + exit 1 +} + +cd squashfs-root +HERE="$PWD" + +echo "提取目录: $HERE" + +# 查找可执行文件(不输出额外信息) +find_executable() { + local dir="$1" + local app_name=$(basename "$APPIMAGE" .AppImage) + + # 1. 尝试与AppImage同名的程序 + if [[ -x "$dir/usr/bin/$app_name" ]]; then + echo "$dir/usr/bin/$app_name" + return 0 + fi + + # 2. 查找usr/bin下的第一个可执行文件 + if [[ -d "$dir/usr/bin" ]]; then + for prog in "$dir/usr/bin"/*; do + if [[ -x "$prog" ]]; then + echo "$prog" + return 0 + fi + done + fi + + # 3. 查找AppRun + if [[ -x "$dir/AppRun" ]]; then + echo "$dir/AppRun" + return 0 + fi + + return 1 +} + +# 静默查找可执行文件 +EXECUTABLE=$(find_executable "$HERE" 2>/dev/null) + +if [[ -z "$EXECUTABLE" ]] || [[ ! -f "$EXECUTABLE" ]]; then + echo "错误: 未找到可执行文件" >&2 + echo "目录内容:" >&2 + ls -la "$HERE" >&2 + if [[ -d "$HERE/usr/bin" ]]; then + echo "usr/bin内容:" >&2 + ls -la "$HERE/usr/bin" >&2 + fi + exit 1 +fi + +echo "可执行文件: $EXECUTABLE" +echo "文件类型: $(file -b "$EXECUTABLE" 2>/dev/null || echo "无法识别")" + +# 运行程序 +ARCH=$(uname -m) + +if [[ "$ARCH" == "x86_64" ]]; then + echo "原生x86_64系统,直接运行..." + exec "$EXECUTABLE" "$@" +else + echo "ARM系统 ($ARCH),使用QEMU..." + + # 确保lib64目录 + if [[ ! -d "$HERE/lib64" ]] && [[ -f "$HERE/usr/lib/ld-linux-x86-64.so.2" ]]; then + mkdir -p "$HERE/lib64" + cp "$HERE/usr/lib/ld-linux-x86-64.so.2" "$HERE/lib64/" 2>/dev/null + fi + + # 直接使用QEMU运行 + exec qemu-x86_64 -L "$HERE" "$EXECUTABLE" "$@" +fi diff --git a/lci/include/Crypto/AES_CBC.hpp b/lci/include/Crypto/AES_CBC.hpp new file mode 100644 index 0000000..b36caf8 --- /dev/null +++ b/lci/include/Crypto/AES_CBC.hpp @@ -0,0 +1,145 @@ +#include "Crypto_Basic.hpp" +#include +#include +#include +#include +#include +#include + + +#define CRYPTO_CHECK(condition, message) \ + if (!(condition)) { \ + throw std::runtime_error(message); \ + } +namespace qing { + class AES_CBC: public qing::Crypto_Basic { + public: + /* 构造函数 初始化一个随即的密钥和初始化向量 */ + AES_CBC() { + generate_key_iv(key, iv); + } + AES_CBC(std::vector &key, std::vector &iv) { + this->key = key; + this->iv = iv; + } + std::string get_key() { + return bytes_to_hex(key); + } + std::string get_iv() { + return bytes_to_hex(iv); + } + + std::string encrypt(std::string original_text) { + std::vector plaintext(original_text.begin(), original_text.end()); + std::vector ciphertext = aes_encrypt(plaintext, key, iv); + + // hex-bytes转换测试 + /*std::string hex = bytes_to_hex(ciphertext); + std::vector cipher2 = hex_to_bytes(hex); + print_bytes(ciphertext); + print_bytes(cipher2);*/ + + return bytes_to_hex(ciphertext); + } + + std::string decrypt(std::string cipher_hex) { + std::vector ciphertext = hex_to_bytes(cipher_hex); + std::vector decrypted = aes_decrypt(ciphertext, key, iv); + return std::string(decrypted.begin(), decrypted.end()); + } + + static void print_bytes(std::vector& bytes) { + for (unsigned char byte : bytes) { + std::cerr << std::setw(2) << static_cast(byte); + } + std::cerr << std::endl; + } + + private: + std::vector key; + std::vector iv; + // 生成随机密钥和IV + static void generate_key_iv(std::vector& key, + std::vector& iv) { + key.resize(32); // AES-256 需要 32 字节密钥 + iv.resize(16); // AES 块大小 16 字节(128-bit) + + // 生成随即密钥 + CRYPTO_CHECK(RAND_bytes(key.data(), key.size()) == 1, "密钥生成失败"); + + // 生成随即IV + CRYPTO_CHECK(RAND_bytes(iv.data(), iv.size()) == 1, "IV生成失败"); + } + + // AES-256-CBC 加密 + static std::vector aes_encrypt(const std::vector& plaintext, + const std::vector& key, + const std::vector& iv) { + // 创建上下文 + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + CRYPTO_CHECK(ctx != nullptr, "无法创建加密上下文"); + + // 初始化加密操作 + CRYPTO_CHECK(EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, + key.data(), iv.data()) , "加密初始化失败"); + + // 输出缓冲区 (密文可能比明文长一个块) + std::vector ciphertext(plaintext.size() + EVP_CIPHER_CTX_block_size(ctx)); + int len = 0; + int ciphertext_len = 0; + + // 提供明文数据 + CRYPTO_CHECK(EVP_EncryptUpdate(ctx, ciphertext.data(), &len, + plaintext.data(), plaintext.size()) == 1, "加载更新失败"); + ciphertext_len = len; + + // 完成加密(处理填充) + CRYPTO_CHECK(EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len) == 1, "加密结束失败"); + ciphertext_len += len; + + // 调整到实际密文长度 + ciphertext.resize(ciphertext_len); + + // 处理上下文 + EVP_CIPHER_CTX_free(ctx); + + return ciphertext; + + } + + // AES-256-CBC + static std::vector aes_decrypt(const std::vector& ciphertext, + const std::vector& key, + const std::vector& iv) { + // 创建上下文 + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + CRYPTO_CHECK(ctx != nullptr, "无法创建解密上下文"); + + // 初始化解密操作 + CRYPTO_CHECK(EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, + key.data(), iv.data()) == 1, "解密初始化失败"); + + // 输出缓冲区(明文可能比密文短,但最大为密文长度) + std::vector plaintext(ciphertext.size()); + int len = 0; + int plaintext_len = 0; + + // 提供密文数据 + CRYPTO_CHECK(EVP_DecryptUpdate(ctx, plaintext.data(), &len, + ciphertext.data(), ciphertext.size()) == 1, "解密更新失败"); + plaintext_len = len; + + // 完成解密(处理填充) + CRYPTO_CHECK(EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len) == 1, "解密结束失败"); + plaintext_len += len; + + // 调整到实际明文长度 + plaintext.resize(plaintext_len); + + // 清理上下文 + EVP_CIPHER_CTX_free(ctx); + + return plaintext; + } + }; +} diff --git a/lci/include/Crypto/Crypto_Basic.hpp b/lci/include/Crypto/Crypto_Basic.hpp new file mode 100644 index 0000000..c931570 --- /dev/null +++ b/lci/include/Crypto/Crypto_Basic.hpp @@ -0,0 +1,55 @@ +#pragma once +#include +#include +#include +namespace qing { + class Crypto_Basic { + public: + class CryptoExp: public std::runtime_error { + public: + CryptoExp(const std::string& msg): std::runtime_error(msg) { + ; + } + }; + protected: + /* 字节码转十六进制字符串 */ + static std::string bytes_to_hex(const std::vector& data) { + static const char hex_chars[] = "0123456789abcdef"; + std::string hex_string; + hex_string.reserve(data.size() * 2); + + for (unsigned char byte: data) { + hex_string.push_back(hex_chars[byte >> 4]); // 高4位 + hex_string.push_back(hex_chars[byte & 0x0F]); // 低4位 + } + + return hex_string; + } + + /* 十六进制字符串转字节码 */ + static std::vector hex_to_bytes(const std::string &hex) { + // 检查长度是否为偶数 + if (hex.size() % 2 != 0) { + throw std::invalid_argument("十六进制字符串长度必须为偶数"); + } + + std::vector bytes; + bytes.reserve(hex.size() / 2); + + auto char_to_nibble = [](char c) -> unsigned char { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); + if (c >= 'A' && c <= 'F') return 10 + (c - 'A'); + throw std::invalid_argument("无效的十六进制字符:" + std::string(1, c)); + }; + + for (size_t i = 0; i < hex.size(); i += 2) { + unsigned char high = char_to_nibble(hex[i]); + unsigned char low = char_to_nibble(hex[i + 1]); + bytes.push_back((high << 4) | low); + } + + return bytes; + } + }; +} diff --git a/lci/include/Crypto/MyRSA.hpp b/lci/include/Crypto/MyRSA.hpp new file mode 100644 index 0000000..c59aa84 --- /dev/null +++ b/lci/include/Crypto/MyRSA.hpp @@ -0,0 +1,288 @@ +#pragma once +#include "Crypto_Basic.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +// 错误处理宏 +#define HANDLE_SSL_ERROR() { \ + char err_buf[512]; \ + ERR_error_string_n(ERR_get_error(), err_buf, sizeof(err_buf)); \ + std::cerr << "SSL error: " << err_buf << std::endl; \ +} +// 自定义删除器用于管理OpenSSL资源 +struct EVP_PKEY_deleter { + void operator()(EVP_PKEY* pkey) const { EVP_PKEY_free(pkey); } +}; +using EVP_PKEY_ptr = std::unique_ptr; + +struct EVP_PKEY_CTX_deleter { + void operator()(EVP_PKEY_CTX* ctx) const { EVP_PKEY_CTX_free(ctx); } +}; +using EVP_PKEY_CTX_ptr = std::unique_ptr; + +struct BIO_deleter { + void operator()(BIO* bio) const { BIO_free_all(bio); } +}; +using BIO_ptr = std::unique_ptr; + +namespace qing { + class MyRSA: public qing::Crypto_Basic { + public: + class Generator { + public: + Generator() { + key = generate_rsa_key(); + } + void save_pri_key(const std::string filename) { + if (!save_private_key(key.get(), filename)) { + throw Crypto_Basic::CryptoExp("保存私钥失败。"); + } + } + void save_pub_key(const std::string filename) { + if(!save_public_key(key.get(), filename)) { + throw Crypto_Basic::CryptoExp("保存公钥失败。"); + } + } + private: + EVP_PKEY_ptr key; + // 生成 RSA 密钥对 + + static EVP_PKEY_ptr generate_rsa_key(size_t key_bits = 2048) { + EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr)); + if (!ctx) { + HANDLE_SSL_ERROR(); + return nullptr; + } + if (EVP_PKEY_keygen_init(ctx.get()) <= 0) { + HANDLE_SSL_ERROR(); + return nullptr; + } + + // 设置 RSA 密钥长度 + OSSL_PARAM params[] = { + OSSL_PARAM_construct_size_t(OSSL_PKEY_PARAM_RSA_BITS, &key_bits), + OSSL_PARAM_END + }; + + if (EVP_PKEY_CTX_set_params(ctx.get(), params) <= 0) { + HANDLE_SSL_ERROR(); + return nullptr; + } + + EVP_PKEY* pkey = nullptr; + if (EVP_PKEY_generate(ctx.get(), &pkey) <= 0) { + HANDLE_SSL_ERROR(); + return nullptr; + } + + return EVP_PKEY_ptr(pkey); + } + + // 保存私钥到文件 + static bool save_private_key(EVP_PKEY* pkey, const std::string& filename, + const char* password = nullptr) { + BIO_ptr bio(BIO_new_file(filename.c_str(), "wb")); + if (!bio) { + std::cerr << "Failed to open file: " << filename << std::endl; + return false; + } + + // 选择加密算法 + const EVP_CIPHER* cipher = password ? EVP_aes_256_cbc() : nullptr; + + int ret = PEM_write_bio_PrivateKey(bio.get(), pkey, cipher, + (unsigned char*) password, + password ? strlen(password) : 0, + nullptr, nullptr); + + if (ret != 1) { + HANDLE_SSL_ERROR(); + return false; + } + + return true; + } + + // 保存公钥到文件 + static bool save_public_key(EVP_PKEY* pkey, const std::string& filename) { + BIO_ptr bio(BIO_new_file(filename.c_str(), "wb")); + if (!bio) { + std::cerr << "Failed ot open file: " << filename << std::endl; + return false; + } + + int ret = PEM_write_bio_PUBKEY(bio.get(), pkey); + if (ret != 1) { + HANDLE_SSL_ERROR(); + return false; + } + + return true; + } + }; + + class Private_Key: Crypto_Basic { + public: + Private_Key(const std::string fullpath) { + if (!(pri_key = load_private_key(fullpath))) + throw Crypto_Basic::CryptoExp("读取私钥失败"); + } + std::string Decrypt(const std::string cipher_hex) { + std::vector ciphertext = hex_to_bytes(cipher_hex); + std::vector decrypted = rsa_decrypt(pri_key.get(), ciphertext); + if (decrypted.empty()) throw Crypto_Basic::CryptoExp("解密失败"); + return std::string(decrypted.begin(), decrypted.end()); + } + private: + EVP_PKEY_ptr pri_key; + // 从文件加载私钥 + EVP_PKEY_ptr load_private_key(const std::string& filename, + const char* password = nullptr) { + BIO_ptr bio(BIO_new_file(filename.c_str(), "rb")); + if (!bio) { + std::cerr << "Failed to open file: " << filename << std::endl; + return nullptr; + } + + // 密码回调函数,显示转换为函数指针类型 + pem_password_cb* callback = +[] (char *buf, int size, int rwflag, void * u) { + if (!u) return 0; + const char* pass = (const char*)u; + int len = strlen(pass); + if (len > size) len = size; + memcpy(buf, pass, len); + return len; + }; + pem_password_cb* cb = password ? nullptr : callback; + + EVP_PKEY* pkey = PEM_read_bio_PrivateKey(bio.get(), nullptr, cb, (void*)password); + if (!pkey) { + HANDLE_SSL_ERROR(); + return nullptr; + } + + return EVP_PKEY_ptr(pkey); + } + + // 使用私钥解密数据 + static std::vector rsa_decrypt(EVP_PKEY* private_key, + const std::vector& ciphertext) { + EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new(private_key, nullptr)); + if (!ctx) { + HANDLE_SSL_ERROR(); + return {}; + } + + if (EVP_PKEY_decrypt_init(ctx.get()) <= 0) { + HANDLE_SSL_ERROR(); + return {}; + } + + // 使用 OAEP 填充 + if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) <= 0) { + HANDLE_SSL_ERROR(); + return {}; + } + + // 获取输出缓冲区大小 + size_t outlen = 0; + if (EVP_PKEY_decrypt(ctx.get(), nullptr, &outlen, + ciphertext.data(), ciphertext.size()) <= 0) { + HANDLE_SSL_ERROR(); + return {}; + } + + // 执行解密 + std::vector plaintext(outlen); + if (EVP_PKEY_decrypt(ctx.get(), plaintext.data(), &outlen, + ciphertext.data(), ciphertext.size()) <= 0) { + HANDLE_SSL_ERROR(); + return {}; + } + + plaintext.resize(outlen); + return plaintext; + } + }; + + + class Public_Key: public Crypto_Basic { + public: + Public_Key(const std::string filename) { + if (!(pub_key = load_public_key(filename))) + throw Crypto_Basic::CryptoExp("读取公钥失败"); + } + std::string Encrypt(std::string original_text) { + std::vector plaintext(original_text.begin(), original_text.end()); + std::vector ciphertext = rsa_encrypt(pub_key.get(), plaintext); + if (ciphertext.empty()) throw Crypto_Basic::CryptoExp("加密失败"); + return bytes_to_hex(ciphertext); + } + private: + EVP_PKEY_ptr pub_key; + + // 从文件加载公钥 + static EVP_PKEY_ptr load_public_key(const std::string& filename) { + BIO_ptr bio(BIO_new_file(filename.c_str(), "rb")); + if (!bio) { + std::cerr << "Failed to open file: " << filename << std::endl; + return nullptr; + } + + EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr); + if (!pkey) { + HANDLE_SSL_ERROR(); + return nullptr; + } + + return EVP_PKEY_ptr(pkey); + } + + // 使用公钥加密数据 + static std::vector rsa_encrypt(EVP_PKEY* public_key, + const std::vector& plaintext) { + EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new(public_key, nullptr)); + if (!ctx) { + HANDLE_SSL_ERROR(); + return {}; + } + + if (EVP_PKEY_encrypt_init(ctx.get()) <= 0) { + HANDLE_SSL_ERROR(); + return {}; + } + + // 使用 OAEP 填充 + if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) <= 0) { + HANDLE_SSL_ERROR(); + return {}; + } + + // 获取输出缓冲区大小 + size_t outlen =0 ; + if (EVP_PKEY_encrypt(ctx.get(), nullptr, &outlen, + plaintext.data(), plaintext.size()) <= 0) { + HANDLE_SSL_ERROR(); + return {}; + } + + // 执行加密 + std::vector ciphertext(outlen); + if (EVP_PKEY_encrypt(ctx.get(), ciphertext.data(), &outlen, + plaintext.data(), plaintext.size()) <= 0) { + HANDLE_SSL_ERROR(); + return {}; + } + + ciphertext.resize(outlen); + return ciphertext; + } + }; + }; +} diff --git a/lci/include/Dashun.h b/lci/include/Dashun.h new file mode 100644 index 0000000..e1ee3a2 --- /dev/null +++ b/lci/include/Dashun.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include "nn/Matrix.hh" +#include "nn/ILayer.hpp" +#include "nn/ILearnable.hpp" +#include "nn/ILayerFullyConnectted.hpp" +#include "nn/ILayerConvolutional.hpp" +#include "nn/NeuralNetwork.hpp" +#include "nn/ConvLayer.hpp" +#include "nn/PoolingLayer.hpp" +#include "nn/FlattenLayer.hpp" +#include "nn/NNBuilder.hpp" +#include "nn/CNNBuilder.hpp" +#include "nn/ImageLoader.hpp" diff --git a/lci/include/Dashun_linux.hpp b/lci/include/Dashun_linux.hpp new file mode 100644 index 0000000..41986c3 --- /dev/null +++ b/lci/include/Dashun_linux.hpp @@ -0,0 +1,44 @@ +#pragma once +/* + * # 安装工具 + * apt install i2c-tools libi2c-dev + * + * # 扫描设备 + * i2cdetect -y 1 + */ +#include "cn/中文化.hpp" + +/* 基本I2C */ +逻辑型 I2C_检查(); +空类型 I2C_初始化(常量 字符* 文件路径 = "/dev/i2c-1"); +空类型 I2C_清除(); +空类型 I2C_获取锁(); /* FIXME: 使用锁类 */ +空类型 I2C_释放锁(); +空类型 I2C_置地址(常量 无号8位整 地址); +空类型 I2C_写寄存器(无号8位整 寄存器, 无号8位整 值); +无号8位整 I2C_读寄存器(无号8位整 寄存器); + +/* PCA9685 */ +逻辑型 PCA9685_检查(); +空类型 PCA9685_初始化(); +空类型 PCA9685_设置PWM(无号8位整 通道, 无号16位整 导通值, 无号16位整 关断值); +空类型 PCA9685_调整角度(无号8位整 通道, 单精度小数 角度); + +/* + * MPU6050 + * 传感器数据 + */ +结构体 MPU6050_传感器数据 { + 有号16位整 加速度计_横, 加速度计_纵, 加速度计_竖; + 有号16位整 陀螺仪_横, 陀螺仪_纵, 陀螺仪_竖; +}; + +逻辑型 MPU6050_检查(); +空类型 MPU6050_初始化(); +MPU6050_传感器数据 MPU6050_读传感器(); /* FIXME: 输入结构体引用(或指针)会更快 */ +空类型 MPU6050_计算角度(常量 MPU6050_传感器数据& 数据, 单精度小数& 俯仰角, 单精度小数& 翻滚角); + + + +/* Robot */ +#include "hd/Robot.hpp" diff --git a/lci/include/cn/中文化.hpp b/lci/include/cn/中文化.hpp new file mode 100644 index 0000000..d5cdddd --- /dev/null +++ b/lci/include/cn/中文化.hpp @@ -0,0 +1,177 @@ + +//作者:简单 +//修改:qing +/* + * C++17支持了中文变量名,配合define预编译指令可以将代码中文化。 + * + * 在C语言中,变量名只能由字母、数字或是下划线组成。 + * 由于C++语言兼容C语言,最开始的C++是不能够使用多字节变量名的。 + * + * */ +#ifndef __中文模式 +#define __中文模式 +#include +/*保留字*/ +#define 使用 using +#define 命名空间 namespace +#define 取别名 typedef +#define 覆盖 override +#define 取内存大小 sizeof +#define 返回 return +#define 如果 if +#define 否则 else +#define 又或者 else if +#define 真 true +#define 假 false +//#define 且 and +//#define 或 or +#define 空类型 void +#define 常量 const +#define 有符号 signed +#define 无符号 unsigned +#define 文件 FILE +#define 文件结束 EOF +#define 类 class +#define 结构体 struct +#define 枚举类型 enum +#define 对象 this +#define 抛出异常 throw +#define 新建 new +//#define 新的 new +#define 删除 delete +#define 跳出 break +#define 继续 continue +#define 判断循环 while +#define 循环判断 do +#define 循环判断尾 while +#define 分支选择 switch +#define 支系 case +#define 缺省 default +#define 计次循环 for +#define 尝试 try +#define 获取异常 catch +#define 捕获异常 catch +#define 退出 exit +#define 公开的 public +#define 受保护的 protected +#define 私有的 private +#define 外部的 extern +#define 静态的 static +#define 虚拟的 virtual + +#define 有号8位整 int8_t +#define 无号8位整 uint8_t +#define 有号16位整 int16_t +#define 无号16位整 uint16_t +#define 有号32位整 int32_t +#define 无号32位整 uint32_t +#define 有号64位整 int64_t +#define 无号64位整 uint64_t + +#define 空的 NULL +#define 空指针 nullptr +#define 整数 int +#define 字符 char +#define 短整数 short +#define 长型 long +#define 超长型 long long +#define 单精度小数 float +#define 双精度小数 double +#define 长度单位 size_t +#define 布尔 bool +#define 逻辑型 bool +#define 可变类型 auto +#define 自动的 auto +#define 适应 auto +#define _文件 FILE +#define _文件结束 EOF +#define _读模式 "r" +#define _写模式 "w" +#define 模板 template +#define 类型名 typename +#define 默认的 default +#define 被删除 delete +#define 入口 main +#define 主函数 main +#define 或 || +#define 或者 || +#define 而且 && +#define 且 && +#define 等于 == +#define 大于等于 >= +#define 小于等于 <= +#define 大于 > +#define 小于 < +#define 不报错的 noexcept +#define 不转换的 explicit +#define 纯函数的 consteval +#define 预求值的 constexpr + +#define _加法重载 operator+ +#define _取下标重载 operator[] +#define _开始 begin +#define _结束 end +//----------------------------------------------------------- +//----------------------------------------------------------- +//基本单元 +#ifdef __cplusplus +#include + +命名空间 中文标准{ + +类 字符串 { +公开的: + /* 初始化为空字符串 */ + 字符串() { + 对象->s = ""; + } + /* 以字面量初始化 */ + 字符串(常量 字符* 源) { + 对象->s = 源; + } + /* 访问器 */ + 常量 字符* 取值() { + 返回 s.c_str(); + } + /* */ + 字符串& _加法重载(常量 字符* 乙方) { + 对象->s += 乙方; + 返回 *对象; + } + /* 字符串之间的连接 */ + 字符串& _加法重载(字符串& 乙) { + 对象->s += 乙.取值(); + 返回 *对象; + } +私有的: + std::string s; +}; + +类 标准输出{ +公开的: + 空类型 调试输出(常量 字符* 消息) { + std::cout << 消息; + } + 空类型 调试输出(字符串& 信) { + std::cout<< 信.取值(); + } +}; + +类 标准输入{ +公开的: + 字符串 取字符串(){ + std::string s; + std::cin >> s; + 返回 字符串(s.c_str()); + } + 整数 取整数() { + 整数 数; + std::cin >> 数; + 返回 数; + } +}; + + +} +#endif //__cplusplus +#endif //__中文模式 diff --git a/lci/include/cn/向量.hpp b/lci/include/cn/向量.hpp new file mode 100644 index 0000000..2380e08 --- /dev/null +++ b/lci/include/cn/向量.hpp @@ -0,0 +1,129 @@ +#pragma once +#include "中文化.hpp" +#include "异常.hpp" +命名空间 中文标准 { +模板 <类型名 类型1> +类 向量{ +公开的: + + /* 注意:当长度单位为0时,内部指针是空的 */ + 向量() { + 对象->指针 = 空指针; + 对象->容量 = 0; + } + /* 专门用于处理花括号初始化列表 */ + 向量(常量 std::initializer_list<类型1>& init = {}) { + 对象->指针 = 新建 类型1[init.size()] {空的}; + 对象->容量 = init.size(); + 长度单位 计数 = 0; + 计次循环(适应 & 东西: init) { + 对象->指针[计数++] = 东西; + } + } + + ~向量() { + 删除 对象->指针; + 对象->指针 = 空指针; + 对象->容量 = 0; + } + /*----------访问器----------*/ + 长度单位 取容量() { + 返回 对象->容量; + } + + 布尔 是否为空() { + 返回 对象->容量 等于 0; + } + + /*--------运算符重载--------*/ + 向量& _取下标重载( 常量 长度单位 下标 ) { + 如果(下标 < 0 或 下标 >= 对象->容量) + 抛出异常 下标越界{"向量", 对象->容量, 下标}; + 返回 对象->指针[下标]; + } + + /*--------可范围遍历--------*/ + 类型1* _开始() { + 返回 对象->指针; + } + + 类型1* _结束() { + 返回 对象->指针 + 对象->容量; + } + + /*----------变异器----------*/ + 空类型 插入元素(常量 长度单位 位置, 常量 向量& 媒介) { + + /* 越界检查 */ + 如果(位置 < 0 或 位置 > 对象->容量) + 抛出异常 下标越界{"向量元素插入", 对象->容量+1, 位置}; + + 适应 长度 = 取容量(); + 适应 新的 = 新建 类型1[长度 + 1] { 空的 }; + + /* 插入位置之前 */ + 计次循环(适应 计数 = 0; 计数 < 位置; ++计数) + 新的[计数] = 对象->指针[计数]; + + /* 执行插入 */ + 新的[位置] = 媒介; + + /* 插入位置之后 */ + 计次循环(适应 计数 = 位置 + 1; 计数 <= 长度; ++计数) + 新的[计数] = 对象->指针[计数 - 1]; + + 如果(对象->指针) 删除[] 对象->指针; /* 可能为空 */ + 对象->指针 = 新的; + } + + 空类型 追加元素(常量 向量& 媒介) { + 适应 长度 = 取容量(); + 适应 新的 = 新建 类型1[长度 +1] {空的}; + 计次循环(适应 计数 = 0; 计数 < 长度; ++计数) + 新的[计数] = 对象->指针[计数]; + 新的[长度] = 媒介; + 如果(对象->指针) 删除[] 指针; /* 可能为空 */ + 对象->指针 = 新的; + } + + 空类型 删除元素(常量 长度单位 位置) { + + /* 越界检查 */ + 如果(位置 < 0 或 位置 >= 对象->容量) + 抛出异常 下标越界{"向量元素插入", 对象->容量 + 1, 位置}; + + 适应 长度 = 取容量(); + 适应 新的 = (长度 等于 1) + ? 空指针 : 新建 类型1[长度 - 1] { 空的 }; + + /* 目标位置之前 */ + 计次循环(适应 计数 = 0; 计数 < 位置; ++计数) + 新的[计数] = 对象->指针[计数]; + + /* 目标位置之后 */ + 计次循环(适应 计数 = 位置; 计数 < 长度; ++计数) + 新的[计数] = 对象->指针[计数 + 1]; + + 删除[] 对象->指针; + 对象->指针 = 新的; + } + + + + /*----------------*/ + //类 迭代器{ + //公开的: + // 迭代器(常量 std::vector<类型1>::iterator& it) { + // this->it = it; + // } + //私有的: + // std::vector<类型1>::iterator it; + //}; + /*----------------*/ + + +私有的: + 类型1 *指针; + 长度单位 容量; +}; +} \ No newline at end of file diff --git a/lci/include/cn/异常.hpp b/lci/include/cn/异常.hpp new file mode 100644 index 0000000..79208bd --- /dev/null +++ b/lci/include/cn/异常.hpp @@ -0,0 +1,24 @@ +#pragma once +#include "中文化.hpp" +命名空间 中文标准 { +类 标准异常 : 公开的 std::exception { +公开的: + 不转换的 标准异常(常量 字符* 信息): 信息(信息) {} + 常量 字符* what() 常量 不报错的 覆盖 { + 返回 信息; + } +私有的: + 常量 字符* 信息; +}; + +类 下标越界 : 公开的 标准异常{ +公开的: + 下标越界(常量 字符 * 提示, 常量 长度单位 标准值, 常量 长度单位 目前值) + : 标准异常(提示), 标准值(标准值), 目前值(目前值) { +}; + 长度单位 取标准值() { 返回 标准值; } + 长度单位 取目前值() { 返回 目前值; } +私有的: + 长度单位 标准值, 目前值; +}; +} \ No newline at end of file diff --git a/lci/include/cn/链串.hpp b/lci/include/cn/链串.hpp new file mode 100644 index 0000000..cd5a9ec --- /dev/null +++ b/lci/include/cn/链串.hpp @@ -0,0 +1,97 @@ +#pragma once +#include "中文化.hpp" +#include "异常.hpp" +#include "顺序表.hpp" +命名空间 中文标准 { +模板 <类型名 类型1, 长度单位 节点大小 = 64> +类 链串 { +公开的: + /* 初始化为空链串 */ + 链串() { + 对象->长度 = 0; + 对象->下一个 = 空指针; + 对象->上一个 = 空指针; + 对象->头 = 对象; /* 每一个子节点都记录同一个头结点 */ + 对象->尾 = 对象; /* 为了方便,只有头结点记录尾节点 */ + } + + 链串 (常量 std::initializer_list<类型1>& init = {}) + { + 计次循环(适应 & 东西: init) { + 插入(东西); + } + } + + /* + * 链式析构 + * + * 析构时,会递归析构所有节点。 + */ + ~链串() { + 删除[] 对象->下一个; + } + + 空类型 插入(常量 类型1& 元素) { + 链串 *暂时 = 对象->尾; + 如果(暂时->长度 等于 节点大小) + _推(); + 暂时->存储[暂时->长度++] = 元素; + } + + 长度单位 统计() { + 长度单位 总数 = 0; + 链串 *暂时 = 对象->头; + 判断循环(暂时 != 空指针) { + 总数 += 暂时->长度; + 暂时 = 暂时->下一个; + } + 返回 总数; + } + + /* + * 链串重组 + * + * 将链串重组为顺序表W + */ + 顺序表<类型1> 重组() { + 顺序表<类型1> 结果(统计()); + 链串 *暂时 = 对象->头; + 长度单位 计数 = 0; + 判断循环(暂时 != 空指针) { + 计次循环(适应 索引 = 0; 索引 < 暂时->长度; ++索引) { + 结果[计数++] = 暂时->存储[索引]; + } + 暂时 = 暂时->下一个; + } + 返回 结果; + } + + 类 链串异常: 公开的 标准异常{ + 公开的: + 链串异常(常量 字符* 消息) : 标准异常(消息) {} + }; +私有的: + 类型1 存储[节点大小]; + 长度单位 长度; /* 存储中的元素数量 */ + 链串 *下一个; + 链串 *上一个; + 链串 *头; + 链串 *尾; + + 空类型 _推() { + 链串*新的; + 尝试{ 新的 = 新建 链串(); } + 捕获异常 (std::exception& exp){ + 抛出异常 链串异常{"链串推失败"}; + } + _拼接节点(新的); + } + + 空类型 _拼接节点(链串* 新的) { + 对象->尾->下一个 = 新的; + 新的->上一个 = 对象->尾; + 对象->尾 = 新的; + 新的->头 = 对象; + } +}; +} \ No newline at end of file diff --git a/lci/include/cn/顺序表.hpp b/lci/include/cn/顺序表.hpp new file mode 100644 index 0000000..d8b55cc --- /dev/null +++ b/lci/include/cn/顺序表.hpp @@ -0,0 +1,28 @@ +#pragma once +#include "中文化.hpp" +#include "异常.hpp" +命名空间 中文标准 { +模板<类型名 类型1> +类 顺序表 { +公开的: + 顺序表(长度单位 大小) { + 存储 = 新建 类型1[大小]; + 长度 = 大小; + } + ~顺序表() { + 删除[] 存储; + } + 类型1 _取下标重载(长度单位 下标) { + 如果(下标 >= 长度) + 抛出异常 顺序表异常{"下标超出范围"}; + 返回 存储[下标]; + } + 类 顺序表异常: 公开的 标准异常{ + 公开的: + 顺序表异常(常量 字符* 消息) : 标准异常(消息) {} + }; +私有的: + 类型1 *存储; + 长度单位 长度; +}; +} \ No newline at end of file diff --git a/lci/include/gui/GTK.hpp b/lci/include/gui/GTK.hpp new file mode 100644 index 0000000..7b93f7f --- /dev/null +++ b/lci/include/gui/GTK.hpp @@ -0,0 +1,560 @@ +#ifndef PMC_GUI_GTK_HPP +#define PMC_GUI_GTK_HPP + +/** + * @file GTK.hpp + * @brief GTK库入口封装 + * + * 提供GTK库的统一入口和简化接口,便于在pmc项目中使用GTK进行GUI开发。 + */ + +#include +#include +#include +#include +#include + +namespace pmc { +namespace gui { + +/** + * @class GTKApplication + * @brief GTK应用程序封装类 + * + * 封装GTK应用程序的初始化和运行逻辑,提供简化的API。 + */ +class GTKApplication { +public: + /** + * @brief 构造函数 + * @param app_id 应用程序ID + * @param app_name 应用程序名称 + */ + GTKApplication(const std::string& app_id, const std::string& app_name); + + /** + * @brief 析构函数 + */ + ~GTKApplication(); + + /** + * @brief 初始化GTK应用程序 + * @param argc 命令行参数数量 + * @param argv 命令行参数数组 + * @return 初始化是否成功 + */ + bool initialize(int argc, char** argv); + + /** + * @brief 运行应用程序 + * @return 应用程序退出码 + */ + int run(); + + /** + * @brief 获取GTK应用程序指针 + * @return GtkApplication指针 + */ + GtkApplication* get_app() const { return app_; } + + /** + * @brief 设置应用程序图标 + * @param icon_path 图标文件路径 + * @return 设置是否成功 + */ + bool set_icon(const std::string& icon_path); + + /** + * @brief 设置窗口默认大小 + * @param width 窗口宽度 + * @param height 窗口高度 + */ + void set_default_window_size(int width, int height); + +private: + std::string app_id_; + std::string app_name_; + GtkApplication* app_; + int default_width_; + int default_height_; +}; + +/** + * @class GTKWindow + * @brief GTK窗口封装类 + * + * 封装GTK窗口的创建和管理,提供简化的窗口操作接口。 + */ +class GTKWindow { +public: + /** + * @brief 构造函数 + * @param title 窗口标题 + * @param width 窗口宽度 + * @param height 窗口高度 + */ + GTKWindow(const std::string& title, int width = 800, int height = 600); + + /** + * @brief 析构函数 + */ + ~GTKWindow(); + + /** + * @brief 显示窗口 + */ + void show(); + + /** + * @brief 隐藏窗口 + */ + void hide(); + + /** + * @brief 设置窗口标题 + * @param title 新标题 + */ + void set_title(const std::string& title); + + /** + * @brief 获取窗口标题 + * @return 窗口标题 + */ + std::string get_title() const; + + /** + * @brief 设置窗口大小 + * @param width 宽度 + * @param height 高度 + */ + void set_size(int width, int height); + + /** + * @brief 获取窗口宽度 + * @return 窗口宽度 + */ + int get_width() const; + + /** + * @brief 获取窗口高度 + * @return 窗口高度 + */ + int get_height() const; + + /** + * @brief 获取GTK窗口指针 + * @return GtkWindow指针 + */ + GtkWindow* get_window() const { return window_; } + + /** + * @brief 设置关闭回调函数 + * @param callback 关闭回调函数 + */ + void set_close_callback(std::function callback); + + /** + * @brief 添加子控件 + * @param widget 要添加的控件 + */ + void add_widget(GtkWidget* widget); + + /** + * @brief 设置窗口位置居中 + */ + void center_on_screen(); + +private: + GtkWindow* window_; + GtkWidget* container_; + std::string title_; + int width_; + int height_; + std::function close_callback_; +}; + +/** + * @class GTKButton + * @brief GTK按钮封装类 + * + * 封装GTK按钮的创建和事件处理。 + */ +class GTKButton { +public: + /** + * @brief 构造函数 + * @param label 按钮标签 + */ + GTKButton(const std::string& label); + + /** + * @brief 析构函数 + */ + ~GTKButton(); + + /** + * @brief 设置点击回调函数 + * @param callback 点击回调函数 + */ + void set_click_callback(std::function callback); + + /** + * @brief 获取GTK按钮指针 + * @return GtkButton指针 + */ + GtkButton* get_button() const { return button_; } + + /** + * @brief 设置按钮标签 + * @param label 新标签 + */ + void set_label(const std::string& label); + + /** + * @brief 获取按钮标签 + * @return 按钮标签 + */ + std::string get_label() const; + + /** + * @brief 启用按钮 + */ + void enable(); + + /** + * @brief 禁用按钮 + */ + void disable(); + + /** + * @brief 设置按钮工具提示 + * @param tooltip 工具提示文本 + */ + void set_tooltip(const std::string& tooltip); + +private: + GtkButton* button_; + std::string label_; + std::function click_callback_; +}; + +/** + * @class GTKLabel + * @brief GTK标签封装类 + * + * 封装GTK标签的创建和文本管理。 + */ +class GTKLabel { +public: + /** + * @brief 构造函数 + * @param text 标签文本 + */ + GTKLabel(const std::string& text = ""); + + /** + * @brief 析构函数 + */ + ~GTKLabel(); + + /** + * @brief 设置标签文本 + * @param text 新文本 + */ + void set_text(const std::string& text); + + /** + * @brief 获取标签文本 + * @return 标签文本 + */ + std::string get_text() const; + + /** + * @brief 获取GTK标签指针 + * @return GtkLabel指针 + */ + GtkLabel* get_label() const { return label_; } + + /** + * @brief 设置文本对齐方式 + * @param xalign 水平对齐 (0.0=左对齐, 0.5=居中, 1.0=右对齐) + * @param yalign 垂直对齐 (0.0=顶部, 0.5=居中, 1.0=底部) + */ + void set_alignment(float xalign = 0.0f, float yalign = 0.5f); + + /** + * @brief 设置文本换行 + * @param wrap 是否换行 + */ + void set_wrap(bool wrap); + + /** + * @brief 设置选择模式 + * @param selectable 是否可选择 + */ + void set_selectable(bool selectable); + +private: + GtkLabel* label_; + std::string text_; +}; + +/** + * @class GTKEntry + * @brief GTK输入框封装类 + * + * 封装GTK输入框的创建和文本管理。 + */ +class GTKEntry { +public: + /** + * @brief 构造函数 + * @param placeholder 占位符文本 + */ + GTKEntry(const std::string& placeholder = ""); + + /** + * @brief 析构函数 + */ + ~GTKEntry(); + + /** + * @brief 获取输入文本 + * @return 输入文本 + */ + std::string get_text() const; + + /** + * @brief 设置输入文本 + * @param text 新文本 + */ + void set_text(const std::string& text); + + /** + * @brief 清空输入框 + */ + void clear(); + + /** + * @brief 获取GTK输入框指针 + * @return GtkEntry指针 + */ + GtkEntry* get_entry() const { return entry_; } + + /** + * @brief 设置占位符文本 + * @param placeholder 占位符文本 + */ + void set_placeholder(const std::string& placeholder); + + /** + * @brief 设置输入变化回调函数 + * @param callback 输入变化回调函数 + */ + void set_changed_callback(std::function callback); + + /** + * @brief 设置激活回调函数(回车键) + * @param callback 激活回调函数 + */ + void set_activate_callback(std::function callback); + + /** + * @brief 启用输入框 + */ + void enable(); + + /** + * @brief 禁用输入框 + */ + void disable(); + +private: + GtkEntry* entry_; + std::string placeholder_; + std::function changed_callback_; + std::function activate_callback_; +}; + +/** + * @class GTKTextView + * @brief GTK文本视图封装类 + * + * 封装GTK文本视图的创建和文本管理,支持多行文本编辑。 + */ +class GTKTextView { +public: + /** + * @brief 构造函数 + */ + GTKTextView(); + + /** + * @brief 析构函数 + */ + ~GTKTextView(); + + /** + * @brief 获取文本内容 + * @return 文本内容 + */ + std::string get_text() const; + + /** + * @brief 设置文本内容 + * @param text 新文本 + */ + void set_text(const std::string& text); + + /** + * @brief 追加文本 + * @param text 要追加的文本 + */ + void append_text(const std::string& text); + + /** + * @brief 清空文本 + */ + void clear(); + + /** + * @brief 获取GTK文本视图指针 + * @return GtkTextView指针 + */ + GtkTextView* get_text_view() const { return text_view_; } + + /** + * @brief 获取文本缓冲区 + * @return GtkTextBuffer指针 + */ + GtkTextBuffer* get_buffer() const { return buffer_; } + + /** + * @brief 设置只读模式 + * @param read_only 是否只读 + */ + void set_read_only(bool read_only); + + /** + * @brief 设置文本变化回调函数 + * @param callback 文本变化回调函数 + */ + void set_changed_callback(std::function callback); + + /** + * @brief 滚动到底部 + */ + void scroll_to_bottom(); + +private: + GtkTextView* text_view_; + GtkTextBuffer* buffer_; + std::function changed_callback_; +}; + +/** + * @class GTKBox + * @brief GTK盒子布局容器封装类 + * + * 封装GTK盒子布局容器的创建和子控件管理。 + */ +class GTKBox { +public: + /** + * @brief 构造函数 + * @param orientation 布局方向 (GTK_ORIENTATION_HORIZONTAL 或 GTK_ORIENTATION_VERTICAL) + * @param spacing 子控件间距 + */ + GTKBox(GtkOrientation orientation = GTK_ORIENTATION_VERTICAL, int spacing = 5); + + /** + * @brief 析构函数 + */ + ~GTKBox(); + + /** + * @brief 添加子控件 + * @param widget 要添加的控件 + * @param expand 是否扩展 + * @param fill 是否填充 + * @param padding 内边距 + */ + void add_widget(GtkWidget* widget, bool expand = false, bool fill = false, int padding = 0); + + /** + * @brief 获取GTK盒子指针 + * @return GtkBox指针 + */ + GtkBox* get_box() const { return box_; } + + /** + * @brief 设置间距 + * @param spacing 新间距 + */ + void set_spacing(int spacing); + + /** + * @brief 设置对齐方式 + * @param align 对齐方式 + */ + void set_alignment(GtkAlign align); + + /** + * @brief 设置均匀分配 + * @param homogeneous 是否均匀分配空间 + */ + void set_homogeneous(bool homogeneous); + +private: + GtkBox* box_; + GtkOrientation orientation_; + int spacing_; +}; + +/** + * @brief 初始化GTK库 + * @param argc 命令行参数数量指针 + * @param argv 命令行参数数组指针 + * @return 初始化是否成功 + */ +bool gtk_init(int* argc, char*** argv); + +/** + * @brief 运行GTK主循环 + */ +void gtk_main(); + +/** + * @brief 退出GTK主循环 + */ +void gtk_main_quit(); + +/** + * @brief 显示错误对话框 + * @param parent 父窗口 + * @param title 对话框标题 + * @param message 错误消息 + */ +void show_error_dialog(GtkWindow* parent, const std::string& title, const std::string& message); + +/** + * @brief 显示信息对话框 + * @param parent 父窗口 + * @param title 对话框标题 + * @param message 信息消息 + */ +void show_info_dialog(GtkWindow* parent, const std::string& title, const std::string& message); + +/** + * @brief 显示确认对话框 + * @param parent 父窗口 + * @param title 对话框标题 + * @param message 确认消息 + * @return 用户是否确认 + */ +bool show_confirm_dialog(GtkWindow* parent, const std::string& title, const std::string& message); + +} // namespace gui +} // namespace pmc + +#endif // PMC_GUI_GTK_HPP \ No newline at end of file diff --git a/lci/include/gui/GTK_impl.hpp b/lci/include/gui/GTK_impl.hpp new file mode 100644 index 0000000..4d57b38 --- /dev/null +++ b/lci/include/gui/GTK_impl.hpp @@ -0,0 +1,456 @@ +#ifndef PMC_GUI_GTK_IMPL_HPP +#define PMC_GUI_GTK_IMPL_HPP + +/** + * @file GTK_impl.hpp + * @brief GTK库封装实现模板 + * + * 提供GTK封装类的内联实现,便于头文件包含。 + */ + +#include "gui/GTK.hpp" +#include + +namespace pmc { +namespace gui { + +// GTKApplication 实现 +inline GTKApplication::GTKApplication(const std::string& app_id, const std::string& app_name) + : app_id_(app_id), app_name_(app_name), app_(nullptr), default_width_(800), default_height_(600) { +} + +inline GTKApplication::~GTKApplication() { + if (app_) { + g_object_unref(app_); + } +} + +inline bool GTKApplication::initialize(int argc, char** argv) { + app_ = gtk_application_new(app_id_.c_str(), G_APPLICATION_FLAGS_NONE); + if (!app_) { + std::cerr << "Failed to create GTK application: " << app_name_ << std::endl; + return false; + } + return true; +} + +inline int GTKApplication::run() { + if (!app_) { + std::cerr << "GTK application not initialized" << std::endl; + return -1; + } + return g_application_run(G_APPLICATION(app_), 0, nullptr); +} + +inline bool GTKApplication::set_icon(const std::string& icon_path) { + // 实现图标设置逻辑 + return true; +} + +inline void GTKApplication::set_default_window_size(int width, int height) { + default_width_ = width; + default_height_ = height; +} + +// GTKWindow 实现 +inline GTKWindow::GTKWindow(const std::string& title, int width, int height) + : title_(title), width_(width), height_(height), window_(nullptr), container_(nullptr) { + window_ = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL)); + if (window_) { + gtk_window_set_title(window_, title_.c_str()); + gtk_window_set_default_size(window_, width_, height_); + container_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); + gtk_container_add(GTK_CONTAINER(window_), container_); + } +} + +inline GTKWindow::~GTKWindow() { + // GTK会自动管理窗口内存 +} + +inline void GTKWindow::show() { + if (window_) { + gtk_widget_show_all(GTK_WIDGET(window_)); + } +} + +inline void GTKWindow::hide() { + if (window_) { + gtk_widget_hide(GTK_WIDGET(window_)); + } +} + +inline void GTKWindow::set_title(const std::string& title) { + title_ = title; + if (window_) { + gtk_window_set_title(window_, title_.c_str()); + } +} + +inline std::string GTKWindow::get_title() const { + return title_; +} + +inline void GTKWindow::set_size(int width, int height) { + width_ = width; + height_ = height; + if (window_) { + gtk_window_resize(window_, width_, height_); + } +} + +inline int GTKWindow::get_width() const { + return width_; +} + +inline int GTKWindow::get_height() const { + return height_; +} + +inline void GTKWindow::set_close_callback(std::function callback) { + close_callback_ = callback; + if (window_) { + g_signal_connect(window_, "destroy", G_CALLBACK(+[](GtkWidget* widget, gpointer data) { + auto self = static_cast(data); + if (self && self->close_callback_) { + self->close_callback_(); + } + }), this); + } +} + +inline void GTKWindow::add_widget(GtkWidget* widget) { + if (container_ && widget) { + gtk_container_add(GTK_CONTAINER(container_), widget); + } +} + +inline void GTKWindow::center_on_screen() { + if (window_) { + gtk_window_set_position(window_, GTK_WIN_POS_CENTER); + } +} + +// GTKButton 实现 +inline GTKButton::GTKButton(const std::string& label) + : label_(label) { + button_ = GTK_BUTTON(gtk_button_new_with_label(label_.c_str())); +} + +inline GTKButton::~GTKButton() { + // GTK会自动管理按钮内存 +} + +inline void GTKButton::set_click_callback(std::function callback) { + click_callback_ = callback; + if (button_) { + g_signal_connect(button_, "clicked", G_CALLBACK(+[](GtkButton* button, gpointer data) { + auto self = static_cast(data); + if (self && self->click_callback_) { + self->click_callback_(); + } + }), this); + } +} + +inline void GTKButton::set_label(const std::string& label) { + label_ = label; + if (button_) { + gtk_button_set_label(button_, label_.c_str()); + } +} + +inline std::string GTKButton::get_label() const { + return label_; +} + +inline void GTKButton::enable() { + if (button_) { + gtk_widget_set_sensitive(GTK_WIDGET(button_), TRUE); + } +} + +inline void GTKButton::disable() { + if (button_) { + gtk_widget_set_sensitive(GTK_WIDGET(button_), FALSE); + } +} + +inline void GTKButton::set_tooltip(const std::string& tooltip) { + if (button_) { + gtk_widget_set_tooltip_text(GTK_WIDGET(button_), tooltip.c_str()); + } +} + +// GTKLabel 实现 +inline GTKLabel::GTKLabel(const std::string& text) + : text_(text) { + label_ = GTK_LABEL(gtk_label_new(text_.c_str())); +} + +inline GTKLabel::~GTKLabel() { + // GTK会自动管理标签内存 +} + +inline void GTKLabel::set_text(const std::string& text) { + text_ = text; + if (label_) { + gtk_label_set_text(label_, text_.c_str()); + } +} + +inline std::string GTKLabel::get_text() const { + return text_; +} + +inline void GTKLabel::set_alignment(float xalign, float yalign) { + if (label_) { + gtk_label_set_xalign(label_, xalign); + gtk_label_set_yalign(label_, yalign); + } +} + +inline void GTKLabel::set_wrap(bool wrap) { + if (label_) { + gtk_label_set_line_wrap(label_, wrap); + } +} + +inline void GTKLabel::set_selectable(bool selectable) { + if (label_) { + gtk_label_set_selectable(label_, selectable); + } +} + +// GTKEntry 实现 +inline GTKEntry::GTKEntry(const std::string& placeholder) + : placeholder_(placeholder) { + entry_ = GTK_ENTRY(gtk_entry_new()); + if (!placeholder_.empty()) { + gtk_entry_set_placeholder_text(entry_, placeholder_.c_str()); + } +} + +inline GTKEntry::~GTKEntry() { + // GTK会自动管理输入框内存 +} + +inline std::string GTKEntry::get_text() const { + if (entry_) { + const gchar* text = gtk_entry_get_text(entry_); + return text ? std::string(text) : ""; + } + return ""; +} + +inline void GTKEntry::set_text(const std::string& text) { + if (entry_) { + gtk_entry_set_text(entry_, text.c_str()); + } +} + +inline void GTKEntry::clear() { + if (entry_) { + gtk_entry_set_text(entry_, ""); + } +} + +inline void GTKEntry::set_placeholder(const std::string& placeholder) { + placeholder_ = placeholder; + if (entry_) { + gtk_entry_set_placeholder_text(entry_, placeholder_.c_str()); + } +} + +inline void GTKEntry::set_changed_callback(std::function callback) { + changed_callback_ = callback; + if (entry_) { + g_signal_connect(entry_, "changed", G_CALLBACK(+[](GtkEntry* entry, gpointer data) { + auto self = static_cast(data); + if (self && self->changed_callback_) { + self->changed_callback_(); + } + }), this); + } +} + +inline void GTKEntry::set_activate_callback(std::function callback) { + activate_callback_ = callback; + if (entry_) { + g_signal_connect(entry_, "activate", G_CALLBACK(+[](GtkEntry* entry, gpointer data) { + auto self = static_cast(data); + if (self && self->activate_callback_) { + self->activate_callback_(); + } + }), this); + } +} + +inline void GTKEntry::enable() { + if (entry_) { + gtk_widget_set_sensitive(GTK_WIDGET(entry_), TRUE); + } +} + +inline void GTKEntry::disable() { + if (entry_) { + gtk_widget_set_sensitive(GTK_WIDGET(entry_), FALSE); + } +} + +// GTKTextView 实现 +inline GTKTextView::GTKTextView() { + text_view_ = GTK_TEXT_VIEW(gtk_text_view_new()); + buffer_ = gtk_text_view_get_buffer(text_view_); +} + +inline GTKTextView::~GTKTextView() { + // GTK会自动管理文本视图内存 +} + +inline std::string GTKTextView::get_text() const { + if (buffer_) { + GtkTextIter start, end; + gtk_text_buffer_get_start_iter(buffer_, &start); + gtk_text_buffer_get_end_iter(buffer_, &end); + gchar* text = gtk_text_buffer_get_text(buffer_, &start, &end, FALSE); + std::string result(text ? text : ""); + g_free(text); + return result; + } + return ""; +} + +inline void GTKTextView::set_text(const std::string& text) { + if (buffer_) { + gtk_text_buffer_set_text(buffer_, text.c_str(), -1); + } +} + +inline void GTKTextView::append_text(const std::string& text) { + if (buffer_) { + GtkTextIter end; + gtk_text_buffer_get_end_iter(buffer_, &end); + gtk_text_buffer_insert(buffer_, &end, text.c_str(), -1); + } +} + +inline void GTKTextView::clear() { + if (buffer_) { + gtk_text_buffer_set_text(buffer_, "", -1); + } +} + +inline void GTKTextView::set_read_only(bool read_only) { + if (text_view_) { + gtk_text_view_set_editable(text_view_, !read_only); + } +} + +inline void GTKTextView::set_changed_callback(std::function callback) { + changed_callback_ = callback; + if (buffer_) { + g_signal_connect(buffer_, "changed", G_CALLBACK(+[](GtkTextBuffer* buffer, gpointer data) { + auto self = static_cast(data); + if (self && self->changed_callback_) { + self->changed_callback_(); + } + }), this); + } +} + +inline void GTKTextView::scroll_to_bottom() { + if (text_view_ && buffer_) { + GtkTextIter end; + gtk_text_buffer_get_end_iter(buffer_, &end); + gtk_text_view_scroll_to_iter(text_view_, &end, 0.0, FALSE, 0.0, 0.0); + } +} + +// GTKBox 实现 +inline GTKBox::GTKBox(GtkOrientation orientation, int spacing) + : orientation_(orientation), spacing_(spacing) { + box_ = GTK_BOX(gtk_box_new(orientation_, spacing_)); +} + +inline GTKBox::~GTKBox() { + // GTK会自动管理盒子内存 +} + +inline void GTKBox::add_widget(GtkWidget* widget, bool expand, bool fill, int padding) { + if (box_ && widget) { + gtk_box_pack_start(box_, widget, expand, fill, padding); + } +} + +inline void GTKBox::set_spacing(int spacing) { + spacing_ = spacing; + if (box_) { + gtk_box_set_spacing(box_, spacing_); + } +} + +inline void GTKBox::set_alignment(GtkAlign align) { + if (box_) { + gtk_widget_set_halign(GTK_WIDGET(box_), align); + gtk_widget_set_valign(GTK_WIDGET(box_), align); + } +} + +inline void GTKBox::set_homogeneous(bool homogeneous) { + if (box_) { + gtk_box_set_homogeneous(box_, homogeneous); + } +} + +// 全局函数实现 +inline bool gtk_init(int* argc, char*** argv) { + return gtk_init_check(argc, argv); +} + +inline void gtk_main() { + ::gtk_main(); +} + +inline void gtk_main_quit() { + ::gtk_main_quit(); +} + +inline void show_error_dialog(GtkWindow* parent, const std::string& title, const std::string& message) { + GtkWidget* dialog = gtk_message_dialog_new(parent, + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + "%s", message.c_str()); + gtk_window_set_title(GTK_WINDOW(dialog), title.c_str()); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +inline void show_info_dialog(GtkWindow* parent, const std::string& title, const std::string& message) { + GtkWidget* dialog = gtk_message_dialog_new(parent, + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "%s", message.c_str()); + gtk_window_set_title(GTK_WINDOW(dialog), title.c_str()); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +inline bool show_confirm_dialog(GtkWindow* parent, const std::string& title, const std::string& message) { + GtkWidget* dialog = gtk_message_dialog_new(parent, + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + "%s", message.c_str()); + gtk_window_set_title(GTK_WINDOW(dialog), title.c_str()); + gint response = gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + return response == GTK_RESPONSE_YES; +} + +} // namespace gui +} // namespace pmc + +#endif // PMC_GUI_GTK_IMPL_HPP \ No newline at end of file diff --git a/lci/include/gui/README.md b/lci/include/gui/README.md new file mode 100644 index 0000000..1b57015 --- /dev/null +++ b/lci/include/gui/README.md @@ -0,0 +1,174 @@ +# PMC GUI 模块 - GTK 封装 + +## 概述 + +本模块提供了对GTK库的C++封装,简化在pmc项目中使用GTK进行GUI开发的过程。封装提供了面向对象的接口,隐藏了GTK的C语言细节。 + +## 文件结构 + +``` +include/gui/ +├── GTK.hpp # 主封装头文件(类声明) +├── GTK_impl.hpp # 内联实现文件 +├── gui.hpp # GUI模块主入口 +├── example.hpp # 使用示例 +└── README.md # 本文档 +``` + +## 主要类 + +### 1. GTKApplication +封装GTK应用程序的初始化和运行。 + +```cpp +pmc::gui::GTKApplication app("com.example.pmc", "PMC Application"); +app.initialize(argc, argv); +app.run(); +``` + +### 2. GTKWindow +封装GTK窗口的创建和管理。 + +```cpp +pmc::gui::GTKWindow window("My Window", 800, 600); +window.set_close_callback([]() { + std::cout << "Window closing" << std::endl; + gtk_main_quit(); +}); +window.show(); +``` + +### 3. GTKButton +封装GTK按钮的创建和事件处理。 + +```cpp +pmc::gui::GTKButton button("Click Me"); +button.set_click_callback([]() { + std::cout << "Button clicked!" << std::endl; +}); +``` + +### 4. GTKEntry +封装GTK输入框的创建和文本管理。 + +```cpp +pmc::gui::GTKEntry entry("Enter text here..."); +entry.set_activate_callback([]() { + std::string text = entry.get_text(); + std::cout << "Entered: " << text << std::endl; +}); +``` + +### 5. GTKTextView +封装GTK文本视图的创建和文本管理。 + +```cpp +pmc::gui::GTKTextView text_view; +text_view.set_read_only(true); +text_view.append_text("Hello, World!\n"); +``` + +### 6. GTKBox +封装GTK盒子布局容器的创建和子控件管理。 + +```cpp +pmc::gui::GTKBox vbox(GTK_ORIENTATION_VERTICAL, 5); +vbox.add_widget(GTK_WIDGET(button.get_button())); +vbox.add_widget(GTK_WIDGET(entry.get_entry())); +``` + +## 使用示例 + +### 简单聊天窗口 + +```cpp +#include "gui/example.hpp" + +int main(int argc, char** argv) { + pmc::gui::example::run_simple_chat_example(argc, argv); + return 0; +} +``` + +### 设置窗口 + +```cpp +#include "gui/example.hpp" + +int main(int argc, char** argv) { + pmc::gui::example::run_settings_example(argc, argv); + return 0; +} +``` + +## 编译要求 + +### 依赖项 +- GTK+ 3.0 或更高版本 +- C++11 或更高版本 + +### Ubuntu/Debian 安装依赖 +```bash +sudo apt-get install libgtk-3-dev +``` + +### Fedora/RHEL 安装依赖 +```bash +sudo dnf install gtk3-devel +``` + +### 编译命令示例 +```bash +g++ -std=c++11 -o myapp main.cpp `pkg-config --cflags --libs gtk+-3.0` +``` + +## 集成到pmc项目 + +### 1. 在CMakeLists.txt中添加 +```cmake +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK3 REQUIRED gtk+-3.0) + +include_directories(${GTK3_INCLUDE_DIRS}) +target_link_libraries(your_target ${GTK3_LIBRARIES}) +``` + +### 2. 使用封装类 +```cpp +#include "gui/GTK.hpp" + +// 使用pmc::gui命名空间中的类 +``` + +## 设计原则 + +1. **简化接口**:隐藏GTK的C语言细节,提供C++面向对象接口 +2. **资源管理**:使用RAII原则管理GTK对象生命周期 +3. **类型安全**:使用C++类型系统减少错误 +4. **可扩展性**:易于添加新的封装类 +5. **向后兼容**:保持与原生GTK API的互操作性 + +## 注意事项 + +1. **线程安全**:GTK不是线程安全的,所有GUI操作必须在主线程进行 +2. **内存管理**:GTK使用引用计数,封装类会自动处理引用 +3. **信号连接**:使用lambda表达式简化信号处理 +4. **错误处理**:提供基本的错误检查和对话框支持 + +## 扩展指南 + +要添加新的GTK控件封装: + +1. 在`GTK.hpp`中声明类 +2. 在`GTK_impl.hpp`中实现内联方法 +3. 提供构造函数、析构函数和主要方法 +4. 添加信号连接支持 +5. 更新示例和文档 + +## 许可证 + +本模块遵循pmc项目的整体许可证。 + +## 贡献 + +欢迎提交问题和拉取请求改进本模块。 \ No newline at end of file diff --git a/lci/include/gui/gui.hpp b/lci/include/gui/gui.hpp new file mode 100644 index 0000000..e8bdfb6 --- /dev/null +++ b/lci/include/gui/gui.hpp @@ -0,0 +1,100 @@ +#ifndef PMC_GUI_HPP +#define PMC_GUI_HPP + +/** + * @file gui.hpp + * @brief pmc项目GUI模块主入口 + * + * 提供GTK GUI模块的统一入口,简化GUI开发。 + */ + +#include "gui/GTK.hpp" + +namespace pmc { +namespace gui { + +/** + * @brief 初始化GUI模块 + * @param argc 命令行参数数量 + * @param argv 命令行参数数组 + * @return 初始化是否成功 + */ +bool init(int argc, char** argv); + +/** + * @brief 运行GUI主循环 + */ +void run(); + +/** + * @brief 退出GUI主循环 + */ +void quit(); + +/** + * @brief 创建主窗口 + * @param title 窗口标题 + * @param width 窗口宽度 + * @param height 窗口高度 + * @return 窗口指针 + */ +GTKWindow* create_main_window(const std::string& title = "PMC Application", + int width = 800, int height = 600); + +/** + * @brief 创建聊天窗口 + * @param parent 父窗口 + * @return 聊天窗口指针 + */ +GTKWindow* create_chat_window(GTKWindow* parent = nullptr); + +/** + * @brief 创建设置窗口 + * @param parent 父窗口 + * @return 设置窗口指针 + */ +GTKWindow* create_settings_window(GTKWindow* parent = nullptr); + +/** + * @brief 显示关于对话框 + * @param parent 父窗口 + */ +void show_about_dialog(GTKWindow* parent); + +/** + * @brief 显示状态栏 + * @param parent 父窗口 + * @param message 状态消息 + */ +void show_status_bar(GTKWindow* parent, const std::string& message); + +/** + * @brief 更新状态栏消息 + * @param message 新状态消息 + */ +void update_status_bar(const std::string& message); + +/** + * @brief 显示进度对话框 + * @param parent 父窗口 + * @param title 对话框标题 + * @param message 进度消息 + */ +void show_progress_dialog(GTKWindow* parent, const std::string& title, const std::string& message); + +/** + * @brief 更新进度对话框 + * @param progress 进度值 (0.0-1.0) + * @param message 进度消息 + */ +void update_progress_dialog(double progress, const std::string& message); + +/** + * @brief 关闭进度对话框 + */ +void close_progress_dialog(); + +} // namespace gui +} // namespace pmc + +#endif // PMC_GUI_HPP \ No newline at end of file diff --git a/lci/include/hd/Camera.h b/lci/include/hd/Camera.h new file mode 100644 index 0000000..459322b --- /dev/null +++ b/lci/include/hd/Camera.h @@ -0,0 +1,12 @@ +#pragma once + +/* Camera */ +#define cimg_display 0 /* 不显示图像,只保存 */ +#include "CImg.h" + +void Camera_init(int w, int h); +void Camera_close(); +bool Camera_is_open(); + +/* 在线程安全的情况下取得一帧图像 */ +cimg_library::CImg get_frame_from_camera(bool& flag); \ No newline at end of file diff --git a/lci/include/hd/Robot.hpp b/lci/include/hd/Robot.hpp new file mode 100644 index 0000000..9d68af2 --- /dev/null +++ b/lci/include/hd/Robot.hpp @@ -0,0 +1,241 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "cn/中文化.hpp" +#include "Dashun_linux.hpp" +命名空间 qing{ +类 伺服电机{ /* 180度舵机 */ +公开的: + + /* FIXME: min只能比max小 */ + 伺服电机(整数 通道, 单精度小数 上限=180, 单精度小数 下限=0, 逻辑型 逆向=false) + : 通道(通道), 上限(上限), 下限(下限), 逆向(逆向) {} + + /* 控制舵机运行至一个角度 */ + 空类型 转动至(单精度小数 角度) { + 角度 = std::min(上限, 角度); + 角度 = std::max(下限, 角度); + PCA9685_调整角度( + 通道, (逆向) ? 180 - 角度 : 角度 + ); + } + + /* FIXME: 返回最大值 */ + 单精度小数 取上限() { + 返回 上限; + } + + +私有的: + 整数 通道; /* 通道编号 */ + 单精度小数 上限; /* 角度上限 */ + 单精度小数 下限; /* 角度下限 */ + 逻辑型 逆向; /* 是否反向 */ +}; +} + +命名空间 qing { +类 ActParser{ /* 动作脚本解析器 */ +公开的: + + /* 解析标准输入中的动作脚本 */ + 空类型 fromStdin() { + 计数 = 0; + 清除所有(); + 解析(std::cin); + } + + /* 解析文件输入流中的动作脚本 */ + 空类型 fromFile(const std::string& 路径) { + 计数 = 0; + std::ifstream 文件(路径.c_str(), std::ios::in); + 如果 (!文件.is_open()) + 抛出异常 std::runtime_error("打开文件失败"); + 清除所有(); + 解析(文件); + 文件.close(); + } + + /* 解析字符串流中的动作脚本 */ + 空类型 fromStr(const std::string& 脚本) { + std::stringstream 流(脚本); + 计数 = 0; + 清除所有(); + 解析(流); + } + + /* 执行刚刚解析出来的机器人和动作 */ + 空类型 play() { + 如果 (机组.empty() || 矩阵.empty()) + 抛出异常 std::runtime_error("没动作或机组"); + 机器人扮演(机组, 矩阵); + } + + /* 获得机器人的最大值向量, FIXME: 把逻辑写在外面 */ + std::vector get() { + 适应 结果 = std::vector(); + 计次循环 (适应& 项目: this->机组) { + 结果.push_back(项目.取上限()); + } + /* 这个数组中包含机器人每个舵机的角度上限 */ + 返回 结果; + } + + /* 获取当前这一组动作 */ + std::vector now() { + 如果 (计数 >= 矩阵.size()) + 抛出异常 std::runtime_error("now finished"); + 返回 矩阵[计数]; + } + + /* 执行下一组动作 */ + 空类型 next() { + 如果 (计数 >= 矩阵.size()) + 抛出异常 std::runtime_error("没下一个动作了"); + 适应 临时向量 = 矩阵[计数++]; + 适应 临时矩阵 = std::vector> ({临时向量}); + 机器人扮演(机组, 临时矩阵); + } + + /* 输入下一个动作执行 */ + 空类型 next(std::vector& act) { + 计数++; + auto vv = std::vector> ({act}); + 机器人扮演(机组, vv); + } +private: + 字符 句[16] = {0}; /* FIXME: 这个栈有越界风险 */ + 整数 顶 = 0; + 整数 计数 = 0; + std::vector 数组; /* 临时使用 */ + std::vector> 矩阵; /* 解析出的整套动作 */ + std::vector<伺服电机> 机组; /* 机器人抽象 */ + 空类型 解析(std::istream& 源) { /* NOTE: 修改为非静态成员函数能够减少行数 FIXME: 改为使用C++风格的文件操作 */ + 清除临时(); + 字符 c = 0; + 布尔 end_flag = true; + 布尔 comment_flag = false; + 布尔 obj_flag = false; + 判断循环 (end_flag) { /* 255 for arm */ + end_flag = (bool)源.get(c); + //std::cout<= '0' && c <= '9') || c == '+' || c == '-') { /* 读取数字 */ + 句[顶++] = c; + std::cout< 0) { + 单精度小数 角度 = (单精度小数)atoi(句); + 数组.push_back(角度); + std::cout<<角度<{}; + } + } + + 空类型 递交机组() { + 如果 (!数组.empty()) { + std::cout << "接收到机组: " << 数组.size() << std::endl; + 机组 = 机组工厂(数组); + 数组 = std::vector{}; + } + } + + 空类型 清除临时() { + 句[0] = '\0'; + 顶 = 0; + 数组 = std::vector {}; + } + + 空类型 清除所有() { + 句[0] = '\0'; + 顶 = 0; + 数组 = std::vector {}; + 矩阵 = std::vector> {}; + 机组 = std::vector<伺服电机> {}; + } + + /* 机器人工厂,FIXME: 这里可以做成舵机工厂就可以了, */ + 静态的 std::vector<伺服电机> 机组工厂(const std::vector& 上限集) { + std::vector<伺服电机> 机组; + 计次循环 (整数 甲=0; 甲 < 上限集.size(); ++甲) + 机组.push_back( + 伺服电机(甲, std::abs(上限集[甲]), 0, (上限集[甲] >= 0) ? 假 : 真)); + 返回 机组; + } + + /* 机器人移动,输入一组舵机对象和一组目标角度 */ + 静态的 空类型 机器人行动(std::vector<伺服电机>& 机组, std::vector& 动作) { + 如果 (机组.size() + 1 != 动作.size()) + 抛出异常 std::runtime_error( + "动作与机组不符合, 我们有" + + std::to_string(机组.size()) + "和" + std::to_string(动作.size()) + ); + + /* FIXME: 随机下标以消除先后时间差 */ + 计次循环 (int 甲=0; 甲<机组.size(); ++甲) { + 机组[甲].转动至(动作[甲]); + } + /* 最后一组数据是毫秒延时 */ + 预求值的 整数 缩放 = 1000; + usleep(动作[动作.size()-1] * 缩放); + } + + /* 机器人整套动作 */ + 静态的 空类型 机器人扮演( + std::vector<伺服电机>& 机组, std::vector>& 动作集) + { + 计次循环 (适应 动作: 动作集) + 机器人行动(机组, 动作); + } +}; +} diff --git a/lci/include/hd/Serial.hpp b/lci/include/hd/Serial.hpp new file mode 100644 index 0000000..2c5dedc --- /dev/null +++ b/lci/include/hd/Serial.hpp @@ -0,0 +1,206 @@ +/* + * 这是cpp使用linux C手段读写串口的实现。 + * + * 作者在其中加入了“沟通”手段,这种手段主要是针对COPS设备。 + * + * COPS设备的通信手段主要是上位机问,下位机答,一问一答的形式。 + * + * (!)还未进行对于热插拔的支持。 + * + * 20250621 暂时不用了 + */ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace qing { + /* + * 串口类 + * + * 长连接的速度会更快 + */ + class Serial { + private: + /* 串口文件描述符 */ + int fd = 0; + public: + /* 输入设备文件地址,进行构造。打开后通常需要等待一下 */ + Serial(const std::string& path, double wait = 2) { + fd = open(path.c_str(), O_RDWR | O_NOCTTY | O_NDELAY); + if (fd == -1) { + throw DeviceOpenErroring(); + } + + + std::this_thread::sleep_for(std::chrono::milliseconds((long)(wait*1000))); /* 等待串口 */ + } + //释放 + ~Serial() { + if (fd != -1) close(fd); + } + + /* 设置串口 */ + void Set() { + struct termios options; + tcgetattr(fd, &options); + cfsetispeed(&options, B115200); /* 设置波特率 */ + cfsetospeed(&options, B115200); + options.c_cflag |= (CLOCAL | CREAD);// 启用接收和本地模式 + options.c_cflag &= ~PARENB;// 8位数据位,无校验,1位停止位 + options.c_cflag &= ~CSTOPB; + options.c_cflag &= ~CSIZE; + options.c_cflag |= CS8; + options.c_cflag &= ~CRTSCTS;// 禁用硬件流控 + options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);// 原始输入模式 + options.c_oflag &= ~OPOST;// 原始输出模式 + options.c_cc[VMIN] = 0;// 设置超时 - 1秒 + options.c_cc[VTIME] = 10; + tcsetattr(fd, TCSANOW, &options); + tcflush(fd, TCIOFLUSH); /* 清空缓冲区 */ + } + + /* 写数据 每次写之前清空缓冲区数据*/ + void Write(const std::string& msg) { + tcflush(fd, TCIOFLUSH); /* 清空缓冲区 */ + write(fd, msg.c_str(), msg.size()); + } + + /* 读数据 */ + std::string CircleRead() { + char buffer[1024] = {0}; + std::string ret = ""; + ssize_t n; + do { + n = read(fd, buffer, sizeof(buffer)); + if (n == -1 && ret == "") { + throw NoResponse(); + } else if (n == -1 && ret != "") { + return ret; + } else if (n == 0) { + return ret; + } else if (n > 0) { + buffer[n] = '\0'; + ret += buffer; + } + } while (true); + } + + /* 读数据 */ + std::string Read() { + char buffer[1024] = {0}; + std::string ret = ""; + ssize_t n; + n = read(fd, buffer, sizeof(buffer)); + if (n == -1 && ret == "") { + throw NoResponse(); + } else if (n > 0) { + buffer[n] = '\0'; + ret += buffer; + } + return ret; + } + + + /* 发送然后获取 */ + std::string Send_Wait_And_Get(const std::string& snd, double wait) { + Write(snd); + std::this_thread::sleep_for(std::chrono::milliseconds((long)(wait*1000))); + return Read(); + } + + /* 一问一答,尝试一定次数 */ + std::string Try_Comm(const std::string& snd, double wait, int times = 10, int drop_tail = 0) { + int num = 0; + while (true) { + try { + num ++; + if (num > times) break; + std::string msg = Send_Wait_And_Get(snd, wait); + DropTail(msg, drop_tail); + return msg; + } catch (qing::Serial::Serial_Error e) { + std::cerr << e.what() << std::endl; + } + } + throw NoResponse(); + } + + /* 去掉字符串末尾一定数量的字符 */ + static void DropTail(std::string& msg, int n) { + for (int i=0; i GetPorts() { + std::vector ports; + //Linux实现 + DIR *dir; + struct dirent *ent; + const std::regex serial_regex("(ttyS|ttyUSB|ttyACM)[0-9]+"); + if ((dir = opendir("/dev")) != NULL) { + while((ent = readdir(dir)) != NULL) { + std::string name = ent->d_name; + if (std::regex_match(name, serial_regex)) { + ports.push_back("/dev/"+name); + } + } + closedir(dir); + } + return ports; + } + + /* 以线程不安全的方式打印所有的串口 */ + static void UnsafePrintPorts() { + auto v = GetPorts(); + for (const auto& i: v) { + std::cout << i << std::endl; + } + } + + }; +} diff --git a/lci/include/hd/cpu.h b/lci/include/hd/cpu.h new file mode 100644 index 0000000..e887288 --- /dev/null +++ b/lci/include/hd/cpu.h @@ -0,0 +1,93 @@ +#include +#include +#include +#include +#include +#include + +struct CpuData { + std::string cpu_name; + long user; + long nice; + long system; + long idle; + long iowait; + long irq; + long softirq; + long steal; + long guest; + long guest_nice; + + long total() const { + return user + nice + system + idle + iowait + irq + softirq + steal; + } + + long idle_total() const { + return idle + iowait; + } +}; + +class CpuMonitor { +private: + std::vector read_cpu_stats() { + std::vector stats; + std::ifstream file("/proc/stat"); + std::string line; + + while (std::getline(file, line)) { + if (line.substr(0, 3) == "cpu") { + std::istringstream iss(line); + CpuData data; + iss >> data.cpu_name >> data.user >> data.nice >> data.system + >> data.idle >> data.iowait >> data.irq >> data.softirq + >> data.steal >> data.guest >> data.guest_nice; + stats.push_back(data); + } + } + + return stats; + } + +public: + double get_cpu_usage() { + auto stats1 = read_cpu_stats(); + sleep(1); + auto stats2 = read_cpu_stats(); + + if (stats1.empty() || stats2.empty()) { + return -1.0; + } + + long total_diff = stats2[0].total() - stats1[0].total(); + long idle_diff = stats2[0].idle_total() - stats1[0].idle_total(); + + if (total_diff == 0) { + return 0.0; + } + + return 100.0 * (total_diff - idle_diff) / total_diff; + } + + std::vector get_per_cpu_usage() { + auto stats1 = read_cpu_stats(); + sleep(1); + auto stats2 = read_cpu_stats(); + + std::vector usages; + + for (size_t i = 1; i < stats1.size() && i < stats2.size(); i++) { + long total_diff = stats2[i].total() - stats1[1].total(); + long idle_diff = stats2[i].idle_total() - stats1[i].idle_total(); + + if (total_diff > 0) { + usages.push_back(100.0 * (total_diff - idle_diff) / total_diff); + } else { + usages.push_back(0.0); + } + } + + return usages; + } +}; + + diff --git a/lci/include/hd/mem.h b/lci/include/hd/mem.h new file mode 100644 index 0000000..15d718f --- /dev/null +++ b/lci/include/hd/mem.h @@ -0,0 +1,14 @@ +#ifndef HD_MEM_H +#define HD_MEM_H +#include +#include + +struct MemoryInfo { + long totalMemory; /* 总内存(bytes) */ + long freeMemory; /* 空闲内存 */ + long usedMemory; /* 已用内存 */ + double usageRate; /* 使用率(%) */ +}; + +MemoryInfo getMemoryUsageWithSysinfo(); +#endif diff --git a/lci/include/llm/LLMClient.hpp b/lci/include/llm/LLMClient.hpp new file mode 100644 index 0000000..d6cc604 --- /dev/null +++ b/lci/include/llm/LLMClient.hpp @@ -0,0 +1,106 @@ +#ifndef PMC_LLM_CLIENT_HPP +#define PMC_LLM_CLIENT_HPP + +#include +#include +#include +#include +#include + +#include + +#include "net/HttpClient.hpp" + +namespace pmc::llm { + +class LLMClient { +public: + LLMClient(const std::string& api_key) : api_key_(api_key) {} + + // 流式请求(使用同步版本避免线程死锁) + void stream_request(const std::vector>& messages, + std::function callback) { + // 使用智能指针确保HttpClient在回调期间保持存活 + auto http_client = std::make_shared(); + + // 设置请求头 + http_client->set_header("Authorization: Bearer " + api_key_); + http_client->set_header("Content-Type: application/json"); + http_client->set_header("Accept: text/event-stream"); + + // 构建请求JSON + boost::json::object request; + request["model"] = "deepseek-chat"; + request["stream"] = true; + + // 构建消息数组,添加系统消息要求返回Markdown格式 + boost::json::array messages_array; + + // 添加系统消息,要求使用Markdown格式回复 + boost::json::object system_message; + system_message["role"] = "system"; + system_message["content"] = "请使用Markdown格式回复。使用**粗体**表示重要概念,*斜体*表示强调,`代码`表示代码片段,使用列表、标题等Markdown元素来组织内容。"; + messages_array.push_back(system_message); + + // 添加用户消息 + for (const auto& msg : messages) { + boost::json::object message_obj; + message_obj["role"] = msg.first; + message_obj["content"] = msg.second; + messages_array.push_back(message_obj); + } + request["messages"] = messages_array; + + // 转换为字符串 + std::string request_body = boost::json::serialize(request); + + // 创建包装回调,解析JSON并提取文本内容 + auto wrapped_callback = [callback](const std::string& json_str) { + try { + // 解析JSON + boost::json::value json_value = boost::json::parse(json_str); + + // 检查是否是有效的响应 + if (json_value.is_object()) { + const auto& obj = json_value.as_object(); + + // 检查是否有choices数组 + if (obj.contains("choices") && obj.at("choices").is_array()) { + const auto& choices = obj.at("choices").as_array(); + if (!choices.empty() && choices[0].is_object()) { + const auto& choice = choices[0].as_object(); + + // 检查是否有delta对象 + if (choice.contains("delta") && choice.at("delta").is_object()) { + const auto& delta = choice.at("delta").as_object(); + + // 检查是否有content字段 + if (delta.contains("content") && delta.at("content").is_string()) { + const std::string& content = delta.at("content").as_string().c_str(); + callback(content); + return; + } + } + } + } + } + + // 如果JSON解析失败或没有有效内容,发送原始字符串(用于调试) + callback("[" + json_str + "]"); + } catch (const std::exception& e) { + // JSON解析失败,发送原始字符串 + callback("[" + json_str + "]"); + } + }; + + // 发送POST请求 - 使用post_stream_sync方法 + http_client->post_stream_sync("https://api.deepseek.com/v1/chat/completions", request_body, wrapped_callback); + } + +private: + std::string api_key_; +}; + +} // namespace pmc::llm + +#endif // PMC_LLM_CLIENT_HPP \ No newline at end of file diff --git a/lci/include/llm/LLMWebSocketServer.hpp b/lci/include/llm/LLMWebSocketServer.hpp new file mode 100644 index 0000000..3a297b6 --- /dev/null +++ b/lci/include/llm/LLMWebSocketServer.hpp @@ -0,0 +1,133 @@ +#ifndef PMC_LLM_WEBSOCKET_SERVER_HPP +#define PMC_LLM_WEBSOCKET_SERVER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "LLMClient.hpp" +#include "net/WebSocketServer.hpp" + +namespace pmc::llm { + +class LLMWebSocketServer { +public: + LLMWebSocketServer(const std::string& api_key, unsigned short port = 8080) + : api_key_(api_key) + , port_(port) + , running_(false) { + + // 创建WebSocket服务器 + ws_server_ = std::make_unique(port); + } + + ~LLMWebSocketServer() { + stop(); + } + + void start() { + if (running_) { + return; + } + + std::cout << "启动LLM WebSocket服务器,端口: " << port_ << std::endl; + std::cout << "API密钥: " << (api_key_.empty() ? "未设置" : "已设置") << std::endl; + + // 设置WebSocket回调 + ws_server_->onConnect([this](const std::string& client_id, const std::string& remote_address) { + handle_connect(client_id, remote_address); + }); + + ws_server_->onMessage("/", [this](const std::string& client_id, + const std::string& message, + std::function send_callback) { + // 这里我们使用异步发送,所以忽略send_callback参数 + handle_message(client_id, message); + }); + + ws_server_->onDisconnect([this](const std::string& client_id) { + handle_disconnect(client_id); + }); + + // 启动WebSocket服务器 + ws_server_->start(); + + running_ = true; + + std::cout << "服务器已启动,等待客户端连接..." << std::endl; + std::cout << "按Ctrl+C停止服务器" << std::endl; + } + + void stop() { + if (!running_) { + return; + } + + std::cout << "正在停止服务器..." << std::endl; + ws_server_->stop(); + running_ = false; + } + + bool is_running() const { + return running_; + } + +private: + // 处理连接 + void handle_connect(const std::string& client_id, const std::string& remote_address) { + std::cout << "客户端连接: " << client_id << " (" << remote_address << ")" << std::endl; + + // 使用异步线程发送欢迎消息,确保客户端已准备好 + std::thread([this, client_id]() { + // 等待一小段时间,确保客户端连接完全建立 + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // 发送欢迎消息 + ws_server_->sendToClient(client_id, "欢迎连接到LLM WebSocket服务器!"); + ws_server_->sendToClient(client_id, "请发送您的消息,我将为您提供AI助手服务。"); + }).detach(); + } + + // 处理断开连接 + void handle_disconnect(const std::string& client_id) { + std::cout << "客户端断开: " << client_id << std::endl; + } + + // 处理消息 + void handle_message(const std::string& client_id, const std::string& message) { + std::cout << "收到来自客户端 " << client_id << " 的消息: " << message << std::endl; + + // 创建LLM客户端 + LLMClient llm_client(api_key_); + + // 构建消息历史 + std::vector> messages = { + {"user", message} + }; + + // 发送流式请求 + llm_client.stream_request(messages, [this, client_id](const std::string& chunk) { + // 将每个数据块发送给客户端 + ws_server_->sendToClient(client_id, chunk); + }); + + // 发送处理完成标记 + ws_server_->sendToClient(client_id, "[处理完成]"); + } + +private: + std::string api_key_; + unsigned short port_; + bool running_; + std::unique_ptr ws_server_; +}; + +} // namespace pmc::llm + +#endif // PMC_LLM_WEBSOCKET_SERVER_HPP \ No newline at end of file diff --git a/lci/include/logs/Logger.hpp b/lci/include/logs/Logger.hpp new file mode 100644 index 0000000..7d3baa0 --- /dev/null +++ b/lci/include/logs/Logger.hpp @@ -0,0 +1,107 @@ +/* 日志 */ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +enum class LogLevel { + DEBUG, + INFO, + WARNING, + ERROR, + CRITICAL +}; + +namespace qing { + /* + * 这是由Deepseek创作的,用于记录日志的类模块 + * 简化版本,只输出到控制台 + * + * 优化意见:改为存储日志文件 + */ + class Logger { + public: + //获取日志实例(单例模式) + static Logger& getInstance() { + static Logger instance; + return instance; + } + + //初始化日志系统 + void init(LogLevel minLogLevel = LogLevel::INFO, + bool consoleOutput = true) { + std::lock_guard lock(mutex_); + + if (initialized_) { + return; + } + + minLogLevel_ = minLogLevel; + consoleOutput_ = consoleOutput; + initialized_ = true; + } + + // 设置最低日志级别 + void setMinLogLevel(LogLevel level) { + std::lock_guard lock(mutex_); + minLogLevel_ = level; + } + + // 日志输出方法 + void log(LogLevel level, const std::string& message) { + if (level < minLogLevel_ || !consoleOutput_) { + return; + } + + auto now = std::chrono::system_clock::now(); + auto in_time_t = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()) % 1000; + + std::tm tm; + localtime_r(&in_time_t, &tm); + + std::stringstream ss; + ss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S") + << "." << std::setfill('0') << std::setw(3) << ms.count() + << " [" << logLevelToString(level) << "] " + << message; + + std::lock_guard lock(mutex_); + std::cout << ss.str() << std::endl; + } + + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; + + private: + Logger() = default; + ~Logger() = default; + + std::string logLevelToString(LogLevel level) { + switch (level) { + case LogLevel::DEBUG: return "DEBUG"; + case LogLevel::INFO: return "INFO"; + case LogLevel::WARNING: return "WARNING"; + case LogLevel::ERROR: return "ERROR"; + case LogLevel::CRITICAL: return "CRITICAL"; + default: return "UNKNOWN"; + } + } + + std::mutex mutex_; + LogLevel minLogLevel_ = LogLevel::INFO; + bool consoleOutput_ = true; + bool initialized_ = false; + }; +} + +#define LOG_DEBUG(message) qing::Logger::getInstance().log(LogLevel::DEBUG, message) +#define LOG_INFO(message) qing::Logger::getInstance().log(LogLevel::INFO, message) +#define LOG_WARNING(message) qing::Logger::getInstance().log(LogLevel::WARNING, message) +#define LOG_ERROR(message) qing::Logger::getInstance().log(LogLevel::ERROR, message) +#define LOG_CRITICAL(message) qing::Logger::getInstance().log(LogLevel::CRITICAL, message) diff --git a/lci/include/net/Http.hpp b/lci/include/net/Http.hpp new file mode 100644 index 0000000..a06c613 --- /dev/null +++ b/lci/include/net/Http.hpp @@ -0,0 +1,82 @@ +#pragma once +#include // libcpp-httplib-dev +#include // +#include +#include // +#include +namespace fs = std::filesystem; +/* 这里的类型是HTTP服务器要设置的回调函数的类型 */ +using Handler = std::function; +namespace qing { +/* NOTE: 不要删除该类,这个抽象成能够方便重构 */ +class Http { /* 超文本传输协议(HTTP)服务器类 */ +public: + /* 初始化各个变量 */ + Http(std::string addr, int port): addr(addr), port(port) {} + + /* 运行服务器,失败抛出异常 */ + void run() { + //printAddr(); /* 在日志里打印更好 */ + if (!svr.listen(addr.c_str(), port)) + throw StartServerFailed(""); + } + + /* 终止服务器 */ + void stop() { svr.stop(); } + + + /* + * 设置路由和对应的处理函数 + * + * FIXME: 使用一个方法实现两个功能 + */ + void get (std::string path, Handler handler) { svr.Get(path, handler); } + void post(std::string path, Handler handler) { svr.Post(path, handler); } + + /* + * 打印地址 + * + * FIXME: 把这个函数的具体功能放到类的外部 + */ + std::string printAddr() { return std::string("pmcd now listenning on this address: http://") + addr + ":" + std::to_string(port); } + + /* 获取当前执行文件目录 */ + //static std::string getCurrentDirectory() { + // fs::path currentPath = fs::absolute(__FILE__); + // return currentPath.parent_path().string(); + //} + + // 读取文本文件的所有内容 + //static std::string readFile(const std::string& filePath) { + // std::ifstream file(filePath); // 创建一个输入文件流对象,用于读取文件 + // if (!file) { + // std::cerr << "无法打开文件:" << filePath << std::endl; + // return ""; + // } + // std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); /* 使用迭代器读取文件内容到字符串 */ + // file.close(); + // return content; + //} + + class Http_Error: public std::exception { /* HTTP 异常类 */ + private: + std::string message; + public: + explicit Http_Error(const std::string& msg): message(msg) {} + const char* what() const noexcept override { + return message.c_str(); + } + }; + + class StartServerFailed: public Http_Error { /* 开始服务失败 */ + public: + StartServerFailed(const std::string& msg): Http_Error(msg) {} + }; + + +private: + httplib::Server svr; /* 创建HTTP服务器对象(如果需要HTTPS,则使用SSLServer */ + std::string addr; /* 服务地址 */ + int port; /* 端口 */ +}; +} diff --git a/lci/include/net/HttpClient.hpp b/lci/include/net/HttpClient.hpp new file mode 100644 index 0000000..1c5b706 --- /dev/null +++ b/lci/include/net/HttpClient.hpp @@ -0,0 +1,151 @@ +#ifndef PMC_HTTP_CLIENT_HPP +#define PMC_HTTP_CLIENT_HPP + +#include +#include +#include +#include + +namespace pmc::net { + +class HttpClient { +public: + HttpClient() { + std::cout << "HttpClient构造函数: 开始初始化" << std::endl; + curl_ = curl_easy_init(); + if (!curl_) { + throw std::runtime_error("Failed to initialize CURL"); + } + std::cout << "HttpClient构造函数: 初始化完成" << std::endl; + } + + ~HttpClient() { + std::cout << "HttpClient析构函数: 开始清理" << std::endl; + if (curl_) { + curl_easy_cleanup(curl_); + } + if (headers_) { + curl_slist_free_all(headers_); + } + std::cout << "HttpClient析构函数: 清理完成" << std::endl; + } + + void set_header(const std::string& header) { + std::cout << "set_header: 添加header: " << header << std::endl; + headers_ = curl_slist_append(headers_, header.c_str()); + } + + // 同步流式请求 + void post_stream_sync(const std::string& url, const std::string& data, std::function callback) { + std::cout << "post_stream_sync: 开始发送流式请求到 " << url << std::endl; + + if (!curl_) { + throw std::runtime_error("CURL not initialized"); + } + + // 重置CURL句柄 + curl_easy_reset(curl_); + + // 设置URL + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + + // 设置POST请求 + curl_easy_setopt(curl_, CURLOPT_POST, 1L); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str()); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size()); + + // 设置请求头 + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers_); + + // 设置流式回调函数 + std::string buffer; + auto data_pair = std::make_pair(&buffer, &callback); + std::cout << "post_stream_sync: 数据对创建完成" << std::endl; + + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, stream_write_callback); + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &data_pair); + + // 设置超时和连接选项 + curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 30L); + curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, 10L); + + // 执行请求 + std::cout << "post_stream_sync: 执行请求" << std::endl; + CURLcode res = curl_easy_perform(curl_); + std::cout << "post_stream_sync: 数据对清理完成" << std::endl; + + if (res != CURLE_OK) { + std::cout << "post_stream_sync: CURL错误: " << curl_easy_strerror(res) << std::endl; + throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res)); + } + + std::cout << "post_stream_sync: 请求成功" << std::endl; + } + +private: + CURL* curl_ = nullptr; + struct curl_slist* headers_ = nullptr; + + static size_t stream_write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { + // userdata指向一个std::pair*> + auto* data_pair = static_cast*>*>(userdata); + if (!data_pair || !data_pair->first || !data_pair->second) { + std::cout << "stream_write_callback: 无效的用户数据" << std::endl; + return 0; + } + + std::string& buffer = *(data_pair->first); + std::function& callback = *(data_pair->second); + size_t total_size = size * nmemb; + + std::cout << "stream_write_callback: 收到 " << total_size << " 字节数据" << std::endl; + + // 将数据追加到缓冲区 + buffer.append(ptr, total_size); + + // 打印原始数据(用于调试) + std::string raw_data(ptr, total_size); + std::cout << "stream_write_callback: 原始数据: " << raw_data << std::endl; + + // 处理流式数据(按行处理Server-Sent Events格式) + size_t pos = 0; + size_t newline_pos; + + while ((newline_pos = buffer.find('\n', pos)) != std::string::npos) { + std::string line = buffer.substr(pos, newline_pos - pos); + pos = newline_pos + 1; + + std::cout << "stream_write_callback: 处理行: " << line << std::endl; + + // 处理SSE格式的数据(以"data: "开头) + if (line.rfind("data: ", 0) == 0) { + std::string json_str = line.substr(6); + + if (json_str == "[DONE]") { + std::cout << "stream_write_callback: 收到 [DONE]" << std::endl; + callback("[[流式传输完成]]"); + continue; + } + + // 调用回调函数处理JSON数据 + std::cout << "stream_write_callback: 调用回调,数据长度: " << json_str.length() << std::endl; + callback(json_str); + } else if (!line.empty()) { + // 如果不是SSE格式,直接传递给回调(可能是错误消息) + std::cout << "stream_write_callback: 非SSE数据,直接传递: " << line << std::endl; + callback(line); + } + } + + // 移除已处理的数据 + if (pos > 0) { + buffer.erase(0, pos); + } + + return total_size; + } +}; + +} // namespace pmc::net + +#endif // PMC_HTTP_CLIENT_HPP \ No newline at end of file diff --git a/lci/include/net/HttpServer.hpp b/lci/include/net/HttpServer.hpp new file mode 100644 index 0000000..8c2a10f --- /dev/null +++ b/lci/include/net/HttpServer.hpp @@ -0,0 +1,193 @@ +#ifndef HTTPSERVER_HPP +#define HTTPSERVER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace http = beast::http; +namespace asio = boost::asio; +using tcp = boost::asio::ip::tcp; + +namespace pmc { +namespace net { + +/** + * @brief HTTP请求处理器类型定义 + * + * 处理HTTP请求并返回响应 + */ +using HttpRequestHandler = std::function( + const http::request&, + const std::unordered_map&)>; + +/** + * @brief 中间件处理器类型定义 + * + * 用于处理请求链的中间件 + */ +using MiddlewareHandler = std::function&, + http::response&, + const std::unordered_map&)>; + +/** + * @brief HTTP服务器类(合并增强版) + * + * 基于boost::asio和boost::beast的异步HTTP服务器 + * 结合了原始版本的易用性和修复版本的功能完整性 + */ +class HttpServer { +public: + /** + * @brief 构造函数(监听所有接口) + * @param port 监听端口 + * @param threads 工作线程数 + */ + explicit HttpServer(unsigned short port, unsigned int threads = 1); + + /** + * @brief 构造函数(指定监听地址) + * @param address 监听IP地址(如 "0.0.0.0" 或 "127.0.0.1") + * @param port 监听端口 + * @param threads 工作线程数 + */ + explicit HttpServer(const std::string& address, unsigned short port, unsigned int threads = 1); + + /** + * @brief 析构函数 + */ + ~HttpServer(); + + /** + * @brief 启动服务器 + */ + void start(); + + /** + * @brief 停止服务器 + */ + void stop(); + + /** + * @brief 注册GET请求处理器 + * @param path 请求路径 + * @param handler 请求处理器 + */ + void get(const std::string& path, HttpRequestHandler handler); + + /** + * @brief 注册POST请求处理器 + * @param path 请求路径 + * @param handler 请求处理器 + */ + void post(const std::string& path, HttpRequestHandler handler); + + /** + * @brief 注册PUT请求处理器 + * @param path 请求路径 + * @param handler 请求处理器 + */ + void put(const std::string& path, HttpRequestHandler handler); + + /** + * @brief 注册DELETE请求处理器 + * @param path 请求路径 + * @param handler 请求处理器 + */ + void del(const std::string& path, HttpRequestHandler handler); + + /** + * @brief 注册中间件 + * @param middleware 中间件处理器 + */ + void use(MiddlewareHandler middleware); + + /** + * @brief 设置静态文件目录 + * @param path 静态文件目录路径 + */ + void setStaticDirectory(const std::string& path); + + /** + * @brief 获取服务器状态 + * @return 服务器是否正在运行 + */ + bool isRunning() const; + + /** + * @brief 获取服务器端口 + * @return 服务器监听端口 + */ + unsigned short getPort() const; + + /** + * @brief 获取服务器监听地址 + * @return 服务器监听地址 + */ + std::string getAddress() const; + + /** + * @brief 运行服务器(阻塞调用)- 从原始版本继承 + */ + void run(); + + /** + * @brief 显示服务器状态信息 - 从原始版本继承 + */ + void showStatus() const; + +private: + // 内部类声明 + class Session; + class Listener; + + // 服务器配置 + std::string address_; + unsigned short port_; + unsigned int threads_; + std::atomic running_{false}; + + // Boost.Asio上下文 + std::unique_ptr ioc_; + + // 网络组件 + std::shared_ptr listener_; + std::vector worker_threads_; + + // 路由表 + std::unordered_map get_handlers_; + std::unordered_map post_handlers_; + std::unordered_map put_handlers_; + std::unordered_map delete_handlers_; + + // 中间件链 + std::vector middlewares_; + + // 静态文件目录 + std::string static_directory_; + + // 内部方法 + std::unordered_map parseQueryParams(const std::string& query); + + // 请求处理 + http::response handleRequest(const http::request& req); +}; + +} // namespace net +} // namespace pmc + +#endif // HTTPSERVER_HPP \ No newline at end of file diff --git a/lci/include/net/WebSocketClient.hpp b/lci/include/net/WebSocketClient.hpp new file mode 100644 index 0000000..520711b --- /dev/null +++ b/lci/include/net/WebSocketClient.hpp @@ -0,0 +1,63 @@ +#ifndef PMC_NET_WEBSOCKET_CLIENT_HPP +#define PMC_NET_WEBSOCKET_CLIENT_HPP + +#include "WebSocketClientBase.hpp" + +namespace pmc { +namespace net { + +/** + * @brief 基于 Boost.Beast 的异步 WebSocket 客户端(非 SSL 版本) + */ +class WebSocketClient : public WebSocketClientBase { +public: + /** + * @brief 构造函数 + * @param host 服务器主机名或 IP 地址 + * @param port 服务器端口号 + * @param path WebSocket 路径(默认 "/") + */ + WebSocketClient(const std::string& host, const std::string& port, const std::string& path = "/"); + + /** + * @brief 析构函数 + */ + ~WebSocketClient(); + + // 实现 WebSocketClientBase 接口 + void setMessageCallback(MessageCallback callback) override; + void setErrorCallback(ErrorCallback callback) override; + void setConnectCallback(ConnectCallback callback) override; + void setCloseCallback(CloseCallback callback) override; + + bool connect(int timeout_ms = 5000) override; + void disconnect() override; + bool isConnected() const override; + + bool send(const std::string& message) override; + bool sendBinary(const void* data, size_t size) override; + + void setAutoReconnect(bool enable, int interval_ms = 3000) override; + void setHeartbeatInterval(int interval_ms = 30000) override; + + std::string getHost() const override; + std::string getPort() const override; + std::string getPath() const override; + + // SSL/TLS 相关(非 SSL 版本返回默认值) + bool isSecure() const override { return false; } + void setVerifyCertificate(bool verify) override { /* 忽略 */ } + void setCertificateFile(const std::string& cert_file) override { /* 忽略 */ } + void setPrivateKeyFile(const std::string& key_file) override { /* 忽略 */ } + void setCertificateAuthorityFile(const std::string& ca_file) override { /* 忽略 */ } + +private: + // 前向声明实现类 + class Impl; + std::unique_ptr impl_; +}; + +} // namespace net +} // namespace pmc + +#endif // PMC_NET_WEBSOCKET_CLIENT_HPP \ No newline at end of file diff --git a/lci/include/net/WebSocketClientBase.hpp b/lci/include/net/WebSocketClientBase.hpp new file mode 100644 index 0000000..b885893 --- /dev/null +++ b/lci/include/net/WebSocketClientBase.hpp @@ -0,0 +1,61 @@ +#ifndef PMC_NET_WEBSOCKET_CLIENT_BASE_HPP +#define PMC_NET_WEBSOCKET_CLIENT_BASE_HPP + +#include +#include +#include + +namespace pmc { +namespace net { + +/** + * @brief WebSocket 客户端基类 + * + * 定义所有 WebSocket 客户端的公共接口 + */ +class WebSocketClientBase { +public: + // 回调函数类型 + using MessageCallback = std::function; + using ErrorCallback = std::function; + using ConnectCallback = std::function; + using CloseCallback = std::function; + + virtual ~WebSocketClientBase() = default; + + // 回调函数设置 + virtual void setMessageCallback(MessageCallback callback) = 0; + virtual void setErrorCallback(ErrorCallback callback) = 0; + virtual void setConnectCallback(ConnectCallback callback) = 0; + virtual void setCloseCallback(CloseCallback callback) = 0; + + // 连接管理 + virtual bool connect(int timeout_ms = 5000) = 0; + virtual void disconnect() = 0; + virtual bool isConnected() const = 0; + + // 消息发送 + virtual bool send(const std::string& message) = 0; + virtual bool sendBinary(const void* data, size_t size) = 0; + + // 配置选项 + virtual void setAutoReconnect(bool enable, int interval_ms = 3000) = 0; + virtual void setHeartbeatInterval(int interval_ms = 30000) = 0; + + // 信息获取 + virtual std::string getHost() const = 0; + virtual std::string getPort() const = 0; + virtual std::string getPath() const = 0; + + // SSL/TLS 相关 + virtual bool isSecure() const = 0; + virtual void setVerifyCertificate(bool verify) = 0; + virtual void setCertificateFile(const std::string& cert_file) = 0; + virtual void setPrivateKeyFile(const std::string& key_file) = 0; + virtual void setCertificateAuthorityFile(const std::string& ca_file) = 0; +}; + +} // namespace net +} // namespace pmc + +#endif // PMC_NET_WEBSOCKET_CLIENT_BASE_HPP \ No newline at end of file diff --git a/lci/include/net/WebSocketClientSSL.hpp b/lci/include/net/WebSocketClientSSL.hpp new file mode 100644 index 0000000..cd7abe5 --- /dev/null +++ b/lci/include/net/WebSocketClientSSL.hpp @@ -0,0 +1,85 @@ +#ifndef PMC_NET_WEBSOCKET_CLIENT_SSL_HPP +#define PMC_NET_WEBSOCKET_CLIENT_SSL_HPP + +#include "WebSocketClientBase.hpp" +#include +#include + +namespace pmc { +namespace net { + +/** + * @brief 基于 Boost.Beast 的异步 WebSocket 客户端(SSL/TLS 版本) + * + * 支持安全的 WebSocket 连接(wss://) + */ +class WebSocketClientSSL : public WebSocketClientBase { +public: + /** + * @brief 构造函数 + * @param host 服务器主机名或 IP 地址 + * @param port 服务器端口号(通常为 443) + * @param path WebSocket 路径(默认 "/") + */ + WebSocketClientSSL(const std::string& host, const std::string& port, const std::string& path = "/"); + + /** + * @brief 析构函数 + */ + ~WebSocketClientSSL(); + + // 实现 WebSocketClientBase 接口 + void setMessageCallback(MessageCallback callback) override; + void setErrorCallback(ErrorCallback callback) override; + void setConnectCallback(ConnectCallback callback) override; + void setCloseCallback(CloseCallback callback) override; + + bool connect(int timeout_ms = 5000) override; + void disconnect() override; + bool isConnected() const override; + + bool send(const std::string& message) override; + bool sendBinary(const void* data, size_t size) override; + + void setAutoReconnect(bool enable, int interval_ms = 3000) override; + void setHeartbeatInterval(int interval_ms = 30000) override; + + std::string getHost() const override; + std::string getPort() const override; + std::string getPath() const override; + + // SSL/TLS 相关方法 + bool isSecure() const override { return true; } + void setVerifyCertificate(bool verify) override; + void setCertificateFile(const std::string& cert_file) override; + void setPrivateKeyFile(const std::string& key_file) override; + void setCertificateAuthorityFile(const std::string& ca_file) override; + + /** + * @brief 设置 SSL 上下文选项 + * @param options SSL 上下文选项 + */ + void setSSLContextOptions(long options); + + /** + * @brief 设置密码列表 + * @param ciphers 密码列表字符串 + */ + void setCiphers(const std::string& ciphers); + + /** + * @brief 设置 SNI 主机名 + * @param sni_hostname SNI 主机名 + */ + void setSNIHostname(const std::string& sni_hostname); + +private: + // 前向声明实现类 + class Impl; + std::unique_ptr impl_; +}; + +} // namespace net +} // namespace pmc + +#endif // PMC_NET_WEBSOCKET_CLIENT_SSL_HPP \ No newline at end of file diff --git a/lci/include/net/WebSocketServer.hpp b/lci/include/net/WebSocketServer.hpp new file mode 100644 index 0000000..660c6b3 --- /dev/null +++ b/lci/include/net/WebSocketServer.hpp @@ -0,0 +1,176 @@ +// WebSocketServer.hpp - WebSocket服务器头文件 +// 基于Boost.Beast和Boost.Asio + +#ifndef PMC_NET_WEBSOCKETSERVER_HPP +#define PMC_NET_WEBSOCKETSERVER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pmc::net { + +/** + * @brief WebSocket连接处理器类型定义 + * + * 处理WebSocket连接建立后的消息收发 + */ +using WebSocketMessageHandler = std::function sendCallback)>; + +/** + * @brief WebSocket连接回调类型定义 + */ +using WebSocketConnectHandler = std::function; + +/** + * @brief WebSocket断开连接回调类型定义 + */ +using WebSocketDisconnectHandler = std::function; + +/** + * @brief WebSocket服务器类 + * + * 基于Boost.Beast和Boost.Asio实现的WebSocket服务器 + */ +class WebSocketServer { +public: + /** + * @brief 构造函数 + * @param port 监听端口 + * @param threads 线程数(默认2) + */ + WebSocketServer(unsigned short port, unsigned int threads = 2); + + /** + * @brief 析构函数 + */ + ~WebSocketServer(); + + /** + * @brief 启动服务器 + */ + void start(); + + /** + * @brief 停止服务器 + */ + void stop(); + + /** + * @brief 注册消息处理器 + * @param path 路径(用于路由) + * @param handler 消息处理器 + */ + void onMessage(const std::string& path, WebSocketMessageHandler handler); + + /** + * @brief 注册连接处理器 + * @param handler 连接处理器 + */ + void onConnect(WebSocketConnectHandler handler); + + /** + * @brief 注册断开连接处理器 + * @param handler 断开连接处理器 + */ + void onDisconnect(WebSocketDisconnectHandler handler); + + /** + * @brief 向指定客户端发送消息 + * @param clientId 客户端ID + * @param message 消息内容 + * @return 是否发送成功 + */ + bool sendToClient(const std::string& clientId, const std::string& message); + + /** + * @brief 向所有客户端广播消息 + * @param message 消息内容 + * @return 成功发送的客户端数量 + */ + size_t broadcast(const std::string& message); + + /** + * @brief 断开指定客户端连接 + * @param clientId 客户端ID + */ + void disconnectClient(const std::string& clientId); + + /** + * @brief 获取当前连接的客户端数量 + * @return 客户端数量 + */ + size_t getClientCount() const; + + /** + * @brief 获取服务器是否正在运行 + * @return 运行状态 + */ + bool isRunning() const { return running_; } + + /** + * @brief 获取服务器监听端口 + * @return 端口号 + */ + unsigned short getPort() const { return port_; } + +private: + // 内部Session类 + class Session; + + // 内部Listener类 + class Listener; + + // 内部实现结构体 + struct Impl { + boost::asio::io_context ioc_; + std::shared_ptr listener_; + std::vector threads_; + }; + + // 服务器配置 + unsigned short port_; + unsigned int threads_; + std::atomic running_{false}; + + // 内部实现 + std::unique_ptr impl_; + + // 生成客户端ID + std::string generateClientId() const; + + // 添加客户端 + void addClient(const std::string& clientId, std::shared_ptr session); + + // 移除客户端 + void removeClient(const std::string& clientId); + + // 获取客户端Session + std::shared_ptr getClient(const std::string& clientId); + + // 回调函数 + std::unordered_map messageHandlers_; + WebSocketConnectHandler connectHandler_; + WebSocketDisconnectHandler disconnectHandler_; + + // 客户端管理 + mutable std::mutex clientsMutex_; + std::unordered_map> clients_; +}; + +} // namespace pmc::net + +#endif // PMC_NET_WEBSOCKETSERVER_HPP \ No newline at end of file diff --git a/lci/include/th/Cv_wait.hpp b/lci/include/th/Cv_wait.hpp new file mode 100644 index 0000000..40886f6 --- /dev/null +++ b/lci/include/th/Cv_wait.hpp @@ -0,0 +1,38 @@ +/* + * 利用条件变量进行等待 + * + * FIXME: deepseek 认为有一个设计缺陷:它只能用于单次等待-唤醒的场景。 + * FIXME: 可能导致永久堵塞,应使用带超时的等待 + * FIXME: 使用标准库的 future/promise + */ + +#include +#include /* 也许将字段flag创建为原子变量 */ +#include +#include +namespace qing { +class Cv_wait { /* 用于等待的条件变量类,对同一个对象进行操作能够阻塞当前线程,或者唤醒被阻塞的线程 */ +private: + std::mutex mtx{}; /* 用于控制条件变量的锁 */ + std::condition_variable cv{}; + bool flag = false; /* 用于解锁条件变量的标志 */ +public: + + inline void Wait() /* 线程进入等待 */ + { + auto lock = std::unique_lock(mtx); + flag = false; + cv.wait(lock, [this] { return flag; }); + } + + inline void WakeCv() /* 唤醒线程 */ + { + { + std::lock_guard lcok(mtx); + flag = true; + } + cv.notify_all(); + } + +}; +} diff --git a/lci/include/th/Fsm.hpp b/lci/include/th/Fsm.hpp new file mode 100644 index 0000000..7e9cb28 --- /dev/null +++ b/lci/include/th/Fsm.hpp @@ -0,0 +1,75 @@ +/* + * 有限状态机(Finite State Machine,FSM) + */ + +#pragma once +#include /*上锁 使得有限状态机线程安全*/ +#include /*条件变量,使得子类能够等待状态的设置*/ +namespace qing { +/* +* FiniteStateMachine有限状态机 +* +* 每个数字代表一个状态 使用static_cast<>()之类的函数去赋值 +* FIXME: 之前用过一种使用回调函数处理的方式 +*/ +class Fsm { +public: + + /* 状态枚举类 */ + enum class Stat: int { + SHUT = -1, /* 关闭 */ + STOP = 0, /* 静止 */ + START = 1, /* 启动 */ + RUNNING = 2 /* 运行 */ + /* ... */ + };/*Stat*/ + + + /* 以STOP为初始状态 */ + Fsm() { set(Stat::STOP); } + + enum Stat check() { /* 检查当前的状态 */ + std::lock_guard lk(mtx); + return stat; + } /*check*/ + + + /* + * 在不上锁的情况下设置一个状态 + * 这里不能使用锁,因为我们还要在临界区里唤醒条件变量 + */ + void set_without_lock(Stat stat) { this->stat = stat; } + + /* 带锁的状态设置 但是该方法不唤醒条件变量 */ + void set_without_notify(Stat stat) { + std::lock_guard lk(mtx); + this->stat = stat; + } + + /* 带锁和条件唤醒的状态设置 */ + void set(Stat stat) { + { + std::lock_guard lk(mtx); + set_without_lock(stat); + ready = true; + } + cv.notify_all(); + } + +//protected: + + void suspend() { /* 阻塞线程,等待事件唤醒 */ + std::unique_lock lk(mtx); + ready = false; + cv.wait(lk, [this] { return ready; }); + }/*suspend */ + +private: + std::mutex mtx; /* 线程安全 */ + bool ready = false; /* 用于条件变量的阻塞唤醒 */ + std::condition_variable cv; /* 条件变量 阻塞/唤醒线程 */ + enum Stat stat; /* 状态机的状态变量 */ + +};//Fsm + +}//qing diff --git a/lci/include/th/ITask.hpp b/lci/include/th/ITask.hpp new file mode 100644 index 0000000..95b7616 --- /dev/null +++ b/lci/include/th/ITask.hpp @@ -0,0 +1,9 @@ +#pragma once +namespace qing{ +class ITask { /* 单个任务通用接口 */ +public: + virtual void start() = 0; /* 任务开始 */ + virtual void stop() = 0; /* 任务结束 */ + virtual bool isRunning() = 0; /* 是否在跑 */ +}; +} diff --git a/lci/include/th/Thread.hpp b/lci/include/th/Thread.hpp new file mode 100644 index 0000000..06ad6cb --- /dev/null +++ b/lci/include/th/Thread.hpp @@ -0,0 +1,183 @@ +/* 线程 + * + * + * 包含一个简易的多线程控制机制,需要共享的变量和函数声明为成员。 + * + * 线程主函数包括了流程控制,要使用需要实现其中的各个事件方法 + * + * 20250621: 从派生改为持有 + * + * NOTE: 在对象构造完之后应该激活 + */ + +#pragma once +#include +#include /* 内存管理 */ +#include /* 用于线程休息 */ +#include +#include +#include +#include "th/Fsm.hpp" /* 有限状态机 */ +namespace qing { +class Thread; +//using f_t = void(*)(Thread&); +using f_t = std::function; +class Thread { +public: + + /* 作为可变的函数,持有回调可能比继承更好 */ + Thread(f_t stop_callback, f_t wake_callback, f_t loop_callback, f_t clear_callback); + + /* + * 删除复制构造函数。 + * + * 因为持有线程类,不要轻举妄动。 + */ + Thread(const Thread&) = delete; + + + /* + * 虚析构函数 + * + * FIXME: 具体有什么用,忘了 + */ + virtual ~Thread() {} + + /*-----------------------------------*/ + /*-----------------------------------*/ + /* + * 检查状态的方法 + * + * NOTE: 做成虚函数,万一子类有特别要求呢 + * + * FIXME: 将检查器和变异器做成一个简单的回调函数 + */ + virtual Fsm::Stat check() { + return fsm.check(); + } + + /*-----------------------------------*/ + /*-----------------------------------*/ + /* 让线程能够阻塞直到新的状态被赋予的方法 */ + inline void suspend() { + fsm.suspend(); + } + + /*-----------------------------------*/ + /*-----------------------------------*/ + /* 状态改变的方法 - 变异器 */ + + /* + * 进入唤醒状态 + * + * 唤醒函数不可以进行该类中的锁操作,否则会造成死锁 + */ + virtual void wake() { + //{ + //std::lock_guard lk(mtx_th); + fsm.set(Fsm::Stat::START); + //ready = true; + //} + //cv.notify_all(); + } /* wake */ + + /* 进入静止状态 + * + * 上锁、唤醒阻塞的线程 + */ + virtual void stop() { + { + std::lock_guard lk(mtx_th); + fsm.set(Fsm::Stat::STOP); + ready = true; + } + /* 可能会出现多个线程在等待的情况 */ + cv.notify_all(); + }/* stop */ + + /* 进入关闭状态 */ + virtual void shut() { + { + std::lock_guard lk(mtx_th); + fsm.set(Fsm::Stat::SHUT); + ready = true; + } + cv.notify_all(); + } /*shut*/ + + /* 进入运行状态 */ + virtual void run() { + { + std::lock_guard lk(mtx_th); + fsm.set(Fsm::Stat::RUNNING); + ready = true; + } + cv.notify_all(); + }/*run*/ + + /*-----------------------------------*/ + /*-----------------------------------*/ + + /* + * 激活线程,申请线程对象 + * + * 该函数不能放在构造函数内,因为需要确保对象已经被构造了 + */ + void Activate() { + + /* 防止重复初始化 + * FIXME: 抛出异常?感觉也不太合适 */ + if (th) return; + + /* 申请线程资源 */ + th = std::make_unique(&Thread::main, this); + + } + + /* + * 尝试唤醒线程并等待它开始 据说该函数有问题可能导致异常 + */ + void WaitStart() { + std::unique_lock lk(mtx_th); /* 不进入临界区可能会造成死等,在唤醒过程非常短暂的情况下 */ + wake(); /* 这就是为什么wake()不可以获取临界区,否则会造成死锁 */ + ready = false; /* 这个临界区结束后,WakeEvent()中的run()会唤醒线程的堵塞 */ + cv.wait(lk, [this] { return ready; }); + } + + /* 等待线程关闭 */ + void WaitClose() { + shut(); + if (th->joinable()) + th->join(); + } + + /*-----------------------------------*/ + /* 线程主函数 */ + void main(); + +private: + + qing::Fsm fsm{}; /* 持有状态机类型 */ + + std::unique_ptr th; /* 持有线程类型 */ + + std::mutex mtx_th; /* 持有锁类型 */ + + /* + * 这里持有一个条件变量,用于阻塞以等待线程的关闭 + * + * 注意状态机中的cv是另一个变量 + */ + std::condition_variable cv; + + /* 辅助条件变量的逻辑型 */ + bool ready; + + /* 事件回调 */ + std::unique_ptr StopEvent; + std::unique_ptr WakeEvent; + std::unique_ptr LoopEvent; + std::unique_ptr ClearEvent; + +}; +} diff --git a/lci/include/tt/Mq.h b/lci/include/tt/Mq.h new file mode 100644 index 0000000..644a354 --- /dev/null +++ b/lci/include/tt/Mq.h @@ -0,0 +1,93 @@ +/* + * 目前是Message Queue 的发送端(不涉及创建消息队列) + * + * FIXME: 集成发送、接收和创建 + */ + +#pragma once +#include +#include +#include +#include + +namespace qing { +namespace bip = boost::interprocess; +using sf_t = std::function; /* success */ +using ef_t = std::function; /* error */ +class Mq { +public: + enum {CREATOR=0, USER=1}; + Mq(const std::string& name, int msglen, size_t msgcnt, int mode = USER) { + this->name = std::string("/") + name; + this->msglen = msglen; + this->msgcnt = msgcnt; + this->mode = mode; + switch (mode) { + case USER: + mqp = std::make_unique(bip::open_only, this->name.c_str()); + break; + case CREATOR: /* 由创建者创建消息队列 */ + bip::message_queue::remove(this->name.c_str()); + mqp = std::make_unique( + bip::create_only, this->name.c_str(), + msgcnt, msglen * sizeof(char) + ); + break; + } + } + + /* 创建者负责清理 */ + ~Mq() { + if (mode == CREATOR) + bip::message_queue::remove(name.c_str()); + } + + /* */ + Mq(const Mq&) = delete; + Mq& operator=(const Mq&) = delete; + + + /* 发送消息 */ + void send(std::string &msg) + { + if (msg.size() + 1 > msglen) /* over the len */ + throw std::length_error("Message queue Send Length over"); + + mqp->send(msg.c_str(), msg.size() + 1, 0); /* include \0 */ + } + + /* 尝试接收消息 */ + void recv(sf_t success, ef_t notyet) { + + auto data = std::make_unique(msglen); /* 接收缓冲区 */ + unsigned int priority; /* 优先级 */ + bip::message_queue::size_type recvd_size; + + try { /* try_receive()返回bool类型 */ + auto r = mqp->try_receive( + data.get(), msglen*sizeof(char), + recvd_size, priority + ); + + if (r) /* 成功接收到数据 */ + success(data.get()); + + else /* 没有收到数据 */ + notyet(); + } + + catch (std::exception &exp) { + std::cerr << "exception type: " << typeid(exp).name() << std::endl; + std::cerr << "exception message: " << exp.what() << std::endl; + } + } + +private: + + /* */ + std::unique_ptr mqp; + std::string name; + size_t msglen=0, msgcnt=0; + int mode; +}; +} diff --git a/lci/include/tt/Pipe.h b/lci/include/tt/Pipe.h new file mode 100644 index 0000000..89f2de8 --- /dev/null +++ b/lci/include/tt/Pipe.h @@ -0,0 +1,155 @@ +#pragma once +#include +#include +#include +#include /* open() */ +#include /* mkfifo() */ +#include + +namespace qing { +class Pipe{ /* 20260309(qing): 该类不会抛出异常 */ +public: + static const int CREATOR = 1; + static const int USER =2; + static const ssize_t PIPE_MAX_LEN = 2048; + Pipe(const std::string& path, int mode) + : fd(-1), isCreator(mode == CREATOR) { + std::string real_path = "/tmp/pst-"; + real_path += path; + fifo_path = real_path; + if (isCreator && mkfifo(fifo_path.c_str(), 0666) == -1 && errno != EEXIST) /* wr_wr_wr */ + perror("mkfifo"); /* 文件已存在是正常情况,其他错误才需要处理 */ + } + + ~Pipe() { + /* 如果打开了文件,则关闭文件 */ + if (fd != -1) close(fd); + /**********************/ + /* 如果管道由该对象创建,就要负责清理 */ + if (isCreator) unlink(fifo_path.c_str()); /* 创建者负责清理 */ + } + Pipe(const Pipe&) = delete; + Pipe& operator=(const Pipe&) = delete; + + /* 这里才是打开文件的指令 - 以读模式打开,可以非阻塞 */ + bool openForRead(bool nonblock = false) { + + /* 设置是否以非阻塞方式打开文件 */ + int flags = O_RDONLY; + if (nonblock) { + flags |= O_NONBLOCK; + } + + fd = open(fifo_path.c_str(), flags); + + if (fd == -1) { + perror("open for read"); + return false; + } + + else { + FD_ZERO(&read_fds); + FD_SET(fd, &read_fds); + if (fd > max_fd) max_fd = fd; + return true; + } + + } + + /* 以写模式打开文件 */ + bool openForWrite(/*bool nonblock = falsei*/) { + + /* 写入管道是不需要阻塞/非阻塞的? */ + int flags = O_WRONLY; + //if (nonblock) { + // flags |= O_NONBLOCK; + //} + fd = open(fifo_path.c_str(), flags); + if (fd == -1) { + perror("open for write"); + return false; + } + + else { + FD_ZERO(&write_fds); + FD_SET(fd, &write_fds); + if (fd > max_fd) max_fd = fd; + return true; + } + + } + + /* 写入管道,如果失败返回-1。成功返回写入的字符数。 */ + ssize_t writePipe(const std::string &data) { + if (fd == -1) { + std::cerr << "Pipe not opened for writing" << std::endl; + return -1; + } + else { + return write(fd, data.c_str(), data.size()); + } + } + + /* 将探测与读取解耦,使用者可以自行决定要不要阻塞 */ + bool waitForRead(int timeout_sec = 0, int timeout_usec = 0) { + + /* 如果文件没打开,等待是失败的 */ + if (fd == -1) return false; + + fd_set temp_fds = read_fds; + struct timeval timeout = {timeout_sec, timeout_usec}; + + int ret = select(max_fd + 1, &temp_fds, nullptr, nullptr, &timeout); + + if (ret == -1) { + perror("select"); + return false; + } + + else { + return ret > 0 && FD_ISSET(fd, &temp_fds); + } + } + + /* 读取数据到字符串,返回实际读取的字节数 */ + ssize_t readPipe(std::string *output) { + + if (fd == -1) { + std::cerr << "Pipe not opened for reading" << std::endl; + return -1; + } + + else + + { + + char buffer[PIPE_MAX_LEN]; + ssize_t n = read(fd, buffer, sizeof(buffer) - 1); + if (n >0) { + buffer[n] = '\0'; + *output = buffer; + } + + else if (n == 0) { + /* 管道写入端关闭 */ + *output = ""; + } + + else { + perror("read"); + } + + return n; + + } + } +private: + std::string fifo_path; + int fd; + bool isCreator; + fd_set read_fds; + fd_set write_fds; + fd_set except_fds; + int max_fd; +}; +} diff --git a/lci/init.txt b/lci/init.txt new file mode 100644 index 0000000..e9a89fa --- /dev/null +++ b/lci/init.txt @@ -0,0 +1,2 @@ +sleep 1000 +sleep 1000 diff --git a/lci/pmcd.service b/lci/pmcd.service new file mode 100644 index 0000000..50eddad --- /dev/null +++ b/lci/pmcd.service @@ -0,0 +1,10 @@ +[Unit] +Description=PMC Http Interfaces +After=network.target + +[Service] +User=qing +Group=qing +ExecStart=pmc -i /etc/pmc/init.txt --serve 5200 +[Install] +WantedBy=multi-user.target diff --git a/lci/src/facerec.py b/lci/src/facerec.py new file mode 100644 index 0000000..5aa25e2 --- /dev/null +++ b/lci/src/facerec.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +""" +扩展技能:人脸位置检测工具 +用于检测图像中的人脸并保存人脸位置坐标(百分比格式)到txt文件 +放置在 ~/.aibox/skills/face_position_detector.py +""" + +import os +import sys +import json +import argparse +import cv2 +import face_recognition +import numpy as np +from datetime import datetime +from typing import List, Optional + + +class FacePositionDetector: + """人脸位置检测工具(检测位置坐标并转换为百分比)""" + + def __init__(self): + # 不再使用.aibox文件夹,使用当前目录 + pass + + def _ensure_dirs(self, dir_path: str): + """确保目录存在""" + os.makedirs(dir_path, exist_ok=True) + + def detect_faces(self, image_path: str, save_coords: bool = True, output_file: Optional[str] = None) -> str: + """ + 检测图像中的人脸并保存位置坐标(百分比格式)到txt文件 + """ + try: + if not os.path.exists(image_path): + return f"❌ 错误:文件不存在 - {image_path}" + + # 使用OpenCV获取图像尺寸 + img_cv = cv2.imread(image_path) + if img_cv is None: + return f"❌ 错误:无法读取图像文件 - {image_path}" + + img_height, img_width = img_cv.shape[:2] + + # 使用face_recognition检测人脸 + image = face_recognition.load_image_file(image_path) + face_locations = face_recognition.face_locations(image) + + if not face_locations: + return "📭 未检测到人脸" + + results = [] + saved_coords = [] + + for i, (top, right, bottom, left) in enumerate(face_locations): + # 转换为百分比坐标 + top_pct = top / img_height + right_pct = right / img_width + bottom_pct = bottom / img_height + left_pct = left / img_width + + face_data = { + "index": i + 1, + "position_absolute": {"top": top, "right": right, "bottom": bottom, "left": left}, + "position_percentage": {"top": top_pct, "right": right_pct, "bottom": bottom_pct, "left": left_pct}, + "size_absolute": {"width": right - left, "height": bottom - top}, + "size_percentage": {"width": (right - left) / img_width, "height": (bottom - top) / img_height}, + "image_dimensions": {"width": img_width, "height": img_height} + } + + # 保存人脸坐标到txt文件(百分比格式) + if save_coords: + try: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # 确定输出文件名 + if output_file: + coords_filename = output_file + else: + base_name = os.path.splitext(os.path.basename(image_path))[0] + coords_filename = f"{base_name}_faces_{timestamp}.txt" + + # 使用当前目录作为保存路径 + coords_filepath = coords_filename + + # 写入百分比坐标数据(保留6位小数) + with open(coords_filepath, 'a') as f: + # 格式:top right bottom left (百分比,空格分隔) + f.write(f"{top_pct:.6f} {right_pct:.6f} {bottom_pct:.6f} {left_pct:.6f}\n") + + saved_coords.append(coords_filename) + + face_data["coords_file"] = coords_filename + face_data["coords_path"] = coords_filepath + except Exception as e: + face_data["save_error"] = str(e) + + results.append(face_data) + + # 构建结果字符串 + result_str = f"✅ 检测到 {len(results)} 张人脸\n" + result_str += f"📏 图像尺寸: {img_width}x{img_height}\n\n" + + for face in results: + result_str += f"👤 人脸 #{face['index']}:\n" + result_str += f" 绝对坐标: top={face['position_absolute']['top']}, right={face['position_absolute']['right']}, " + result_str += f"bottom={face['position_absolute']['bottom']}, left={face['position_absolute']['left']}\n" + result_str += f" 百分比坐标: top={face['position_percentage']['top']:.3f}, right={face['position_percentage']['right']:.3f}, " + result_str += f"bottom={face['position_percentage']['bottom']:.3f}, left={face['position_percentage']['left']:.3f}\n" + result_str += f" 绝对尺寸: {face['size_absolute']['width']}x{face['size_absolute']['height']}\n" + result_str += f" 相对尺寸: {face['size_percentage']['width']:.3f}x{face['size_percentage']['height']:.3f}" + + if 'coords_file' in face: + result_str += f"\n 坐标已保存到: {face['coords_file']}" + + result_str += "\n\n" + + if saved_coords: + # 显示所有保存的坐标文件 + unique_files = list(set(saved_coords)) + result_str += f"📂 人脸坐标(百分比格式)已保存到当前目录:\n" + for filename in unique_files: + result_str += f" 📄 {filename}\n" + + return result_str + + except Exception as e: + return f"❌ 检测失败: {e}" + + +def main(): + """简化后的主函数 - 仅人脸位置检测(百分比坐标)""" + parser = argparse.ArgumentParser(description="人脸位置检测工具(检测坐标并转换为百分比)") + subparsers = parser.add_subparsers(dest="command", help="可用命令") + + # detect命令 + detect_parser = subparsers.add_parser("detect", help="检测图像中的人脸并保存百分比坐标") + detect_parser.add_argument("image_path", help="图片文件路径") + detect_parser.add_argument("--no-save", action="store_true", help="不保存坐标") + detect_parser.add_argument("--output", help="指定输出文件名(默认自动生成)") + + # 工具描述命令 + desc_parser = subparsers.add_parser("description", help="显示工具描述") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + sys.exit(1) + + detector = FacePositionDetector() + + if args.command == "description": + desc = """人脸位置检测工具(百分比坐标) + +功能: +- 检测图像中的人脸 +- 将人脸位置坐标转换为百分比格式 +- 保存百分比坐标到txt文件(格式:top right bottom left,均为0-1之间的浮点数) + +坐标说明: +- top: 人脸顶部位置(占图像高度的比例,0=顶部,1=底部) +- right: 人脸右侧位置(占图像宽度的比例,0=左侧,1=右侧) +- bottom: 人脸底部位置(占图像高度的比例,0=顶部,1=底部) +- left: 人脸左侧位置(占图像宽度的比例,0=左侧,1=右侧) + +目录结构: +- 人脸坐标保存位置: 当前目录 + +使用示例: + facerec.py detect image.jpg # 检测人脸并保存百分比坐标到当前目录 + facerec.py detect image.jpg --no-save # 检测但不保存坐标 + facerec.py detect image.jpg --output my_coords.txt # 指定输出文件名""" + print(desc) + + elif args.command == "detect": + save_coords = not args.no_save + result = detector.detect_faces(args.image_path, save_coords, args.output) + print(result) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lci/src/hd/Camera.cpp b/lci/src/hd/Camera.cpp new file mode 100644 index 0000000..9a37f26 --- /dev/null +++ b/lci/src/hd/Camera.cpp @@ -0,0 +1,535 @@ +/* FIXME: 这个文件非混乱 */ +/* FIXME: 抽象一个服务器类 */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hd/Camera.h" + +#define cimg_display 0 /* 不显示图像,只保存 */ +#include "CImg.h" +#include +using namespace cimg_library; + +/*-----------------------------------*/ +/* Camera */ +static const char* filename = "/dev/video0"; /* FIXME: 不一定 */ +static int width = -1; +static int height = -1; + +/* 为了在获取单帧中使用 */ +static struct v4l2_buffer buf; +static enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + +/*-----------------------------------*/ +/* 静态全局线程对象及相关同步原语 */ +static std::thread camera_thread; +static std::atomic camera_running{false}; +static std::mutex camera_mutex; + +/* device file */ +static int fd = -1; + + +/*-----------------------------------*/ +void Camera_check();/* check if this device support video capture (V4L2_CAP_VIDEO_CAPTURE) */ +void Camera_setVideoFormat(int w, int h);/* Set Video Format */ +void Camera_reqBuf();/* Request Camera Buffer */ +void Camera_setup(); /* mapping buffer and data collection loop */ +void Camera_unsetup(); + +/*-----------------------------------*/ +bool Camera_is_open() { + return fd >= 0; +} + +/* FIXME: 如果在集成操作中出现异常,应该释放资源 */ +void Camera_init(int w, int h) { + + if (Camera_is_open()) + throw std::runtime_error("Camera already opened."); + + fd = open(filename, O_RDWR); + if (fd < 0) + throw std::runtime_error( + "Failed to open device: " + std::string(strerror(errno)) + ); + + Camera_check(); + Camera_setVideoFormat(w, h); + Camera_reqBuf(); + Camera_setup(); +} + +/* */ +void Camera_close() { + Camera_unsetup(); + close(fd); + fd = -1; +} + + +void Camera_check() { + struct v4l2_capability cap; + if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) { + throw std::runtime_error( + "VIDIOC_QUERYCAP failed: " + std::string(strerror(errno)) + ); + } + if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { + throw std::runtime_error("Device does not support video capture"); + } +} + + +/* FIXME: 将Camera_init()中的参数放在这里 */ +void Camera_setVideoFormat(int width, int height) { + struct v4l2_format fmt; + memset(&fmt, 0, sizeof(fmt)); + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + fmt.fmt.pix.width = width; /* */ + fmt.fmt.pix.height = height; /* */ + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; /* usually format: YUYV, MJPEG */ + fmt.fmt.pix.field = V4L2_FIELD_NONE; + if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) + throw std::runtime_error( + "VIDIOC_S_FMT failed: " + std::string(strerror(errno))); + ::width= width; + ::height= height; +} + + +void Camera_reqBuf() { + struct v4l2_requestbuffers req; + memset(&req, 0, sizeof(req)); + req.count = 4; /* the number of buffer */ + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_MMAP; /* the mode of memory mapping */ + + if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) + throw std::runtime_error( + "VIDIOC_REQBUFS failed: " + std::string(strerror(errno))); + +} + +/*------------------------------------------------------------------------------*/ + +void Camera_setup() { + /* mapping buffer (It is needed to handle every buffer in loop) */ + memset(&buf, 0, sizeof(buf)); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = 0; /* Index of the buffer */ + + /* 查询缓冲区信息 */ + if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) { + throw std::runtime_error("VIDIOC_QUERYBUF failed: " + std::string(strerror(errno))); + } + + /* 将缓冲区加入队列 */ + if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) { + throw std::runtime_error("VIDIOC_QBUF failed: " + std::string(strerror(errno))); + } + + /* 开始采集 */ + if (ioctl(fd, VIDIOC_STREAMON, &buf.type) < 0) { /* start collection */ + throw std::runtime_error("VIDIOC_STREAMON failed: " + std::string(strerror(errno))); + } + +} + +void Camera_unsetup() +{ + /* 停止采集流 */ + ioctl(fd, VIDIOC_STREAMOFF, &type); +} + + +/*------------------------------------------------------------------------------*/ +/* + * Convert YUYV frame to CImg object and return it + * + * translate out camera data to CImg format + * + * new version + * + * FIXME: 专门建个库 + */ +CImg YUYV_to_CImg(uint8_t* frameData, int width, int height) { + // 参数检查 + if (!frameData || width <= 0 || height <= 0) { + throw std::invalid_argument("Invalid parameters"); + } + + CImg img(width, height, 1, 3); + + // 使用指针提高效率 + uint8_t* src = frameData; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x += 2) { + uint8_t Y0 = src[0]; + uint8_t U = src[1]; + uint8_t Y1 = src[2]; + uint8_t V = src[3]; + src += 4; + + // 使用更精确的系数 + int R0 = Y0 + ((V - 128) * 1402) / 1000; + int G0 = Y0 - ((U - 128) * 344) / 1000 - ((V - 128) * 714) / 1000; + int B0 = Y0 + ((U - 128) * 1772) / 1000; + + int R1 = Y1 + ((V - 128) * 1402) / 1000; + int G1 = Y1 - ((U - 128) * 344) / 1000 - ((V - 128) * 714) / 1000; + int B1 = Y1 + ((U - 128) * 1772) / 1000; + + // 范围限制 + R0 = std::max(0, std::min(255, R0)); + G0 = std::max(0, std::min(255, G0)); + B0 = std::max(0, std::min(255, B0)); + + R1 = std::max(0, std::min(255, R1)); + G1 = std::max(0, std::min(255, G1)); + B1 = std::max(0, std::min(255, B1)); + + // 设置像素 + img(x, y, 0, 0) = static_cast(R0); + img(x, y, 0, 1) = static_cast(G0); + img(x, y, 0, 2) = static_cast(B0); + + if (x + 1 < width) { + img(x + 1, y, 0, 0) = static_cast(R1); + img(x + 1, y, 0, 1) = static_cast(G1); + img(x + 1, y, 0, 2) = static_cast(B1); + } + } + } + + return img; +} + + +CImg get_frame_from_camera(bool& flag) { + + std::lock_guard lock(camera_mutex); + //if (!camera_running.load()) + // throw std::runtime_error("Camera not running"); + + /* Dequeue to get data */ + memset(&buf, 0, sizeof(buf)); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + + if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) { + perror("dequeue failed"); + throw std::runtime_error("dequeue failed"); + } + + /* 获取当前缓冲区的数据指针 */ + uint8_t *frameData = static_cast(mmap(NULL, buf.length, + PROT_READ | PROT_WRITE, MAP_SHARED, + fd, buf.m.offset)); + + if (frameData == MAP_FAILED) { + ioctl(fd, VIDIOC_QBUF, &buf); + throw std::runtime_error("mmap failed in get_frame_from_camera"); + } + + /* process frameData */ + //std::cout << "Got length: " << buf.length << std::endl; + auto img = YUYV_to_CImg(frameData, width, height); + + /* 解除内存映射 */ + munmap(frameData, buf.length); + + if (ioctl(fd, VIDIOC_QBUF, &buf)<0){ /* join the queue again */ + perror("requeue failed"); + throw std::runtime_error("requeue failed"); + } + + + flag = camera_running.load(); /* 检查下一次是否需要退出 */ + return img; /* 返回图像 */ +} + + + +//CImg YUYV_to_CImg(uint8_t* frameData, int width, int height) { + // 创建CImg图像对象 (width, height, depth=1, spectrum=3 for RGB) +// CImg img(width, height, 1, 3); + + // 遍历图像的每一行和每一列 +// for (int y = 0; y < height; y++) { +// for (int x = 0; x < width; x += 2) { + // YUYV格式:每两个像素占用4个字节 +// int yuyv_index = (y * width + x) * 2; + + // 提取YUV值 +// uint8_t Y0 = frameData[yuyv_index]; +// uint8_t U = frameData[yuyv_index + 1]; +// uint8_t Y1 = frameData[yuyv_index + 2]; +// uint8_t V = frameData[yuyv_index + 3]; + + // 转换第一个像素(x, y) +// int R0 = Y0 + (V - 128) * 1.402; +// int G0 = Y0 - (U - 128) * 0.34414 - (V - 128) * 0.71414; +// int B0 = Y0 + (U - 128) * 1.772; + + // 转换第二个像素(x+1, y) +// int R1 = Y1 + (V - 128) * 1.402; +// int G1 = Y1 - (U - 128) * 0.34414 - (V - 128) * 0.71414; +// int B1 = Y1 + (U - 128) * 1.772; + + // 限制范围到0-255 +// R0 = std::max(0, std::min(255, R0)); +// G0 = std::max(0, std::min(255, G0)); +// B0 = std::max(0, std::min(255, B0)); + +// R1 = std::max(0, std::min(255, R1)); +// G1 = std::max(0, std::min(255, G1)); +// B1 = std::max(0, std::min(255, B1)); + + // 设置第一个像素值 +// img(x, y, 0, 0) = static_cast(R0); +// img(x, y, 0, 1) = static_cast(G0); +// img(x, y, 0, 2) = static_cast(B0); + + // 设置第二个像素值(如果在图像范围内) +// if (x + 1 < width) { +// img(x + 1, y, 0, 0) = static_cast(R1); +// img(x + 1, y, 0, 1) = static_cast(G1); +// img(x + 1, y, 0, 2) = static_cast(B1); +// } +// } +// } + +// return img; +//} + + + +/* + * Convert YUYV pixel at position (x, y) in frame to RGB + * YUYV format: [Y0 U0 Y1 V0] [Y2 U2 Y3 V2] ... + * Each 4 bytes contain 2 pixels (Y0,U0,V0 for pixel0, Y1,U0,V0 for pixel1) + */ +void YUYV_to_RGB(uint8_t* frameData, int width, int height, int x, int y, + uint8_t& r, uint8_t& g, uint8_t& b) { + // 计算像素在YUYV数据中的索引 + int pixel_index = y * width + x; + + // YUYV中每2个像素共享4字节:Y0 U Y1 V + int byte_offset = (pixel_index / 2) * 4; + + // 确定是第一个像素还是第二个像素 + bool is_first_pixel = (pixel_index % 2 == 0); + + uint8_t Y, U, V; + + if (is_first_pixel) { + // 第一个像素:Y0, U, V + Y = frameData[byte_offset]; // Y0 + U = frameData[byte_offset + 1]; // U + V = frameData[byte_offset + 3]; // V + } else { + // 第二个像素:Y1, U, V (与第一个像素共享U和V) + Y = frameData[byte_offset + 2]; // Y1 + U = frameData[byte_offset + 1]; // U (共享) + V = frameData[byte_offset + 3]; // V (共享) + } + + // YUV to RGB conversion formula (integer approximation) + // 注意:YUV值通常范围是Y:16-235, UV:16-240 + // 这里先转换为0-255范围 + int Y_adj = Y - 16; + int U_adj = U - 128; + int V_adj = V - 128; + + // 计算RGB值 + int R = (298 * Y_adj + 409 * V_adj + 128) >> 8; + int G = (298 * Y_adj - 100 * U_adj - 208 * V_adj + 128) >> 8; + int B = (298 * Y_adj + 516 * U_adj + 128) >> 8; + + // 限制范围到0-255 + r = (R < 0) ? 0 : ((R > 255) ? 255 : static_cast(R)); + g = (G < 0) ? 0 : ((G > 255) ? 255 : static_cast(G)); + b = (B < 0) ? 0 : ((B > 255) ? 255 : static_cast(B)); +} + +/* + * Convert YUYV frame to CImg object and save as image + * + * FIXME(20251229): 每次处理一个像素都要调用YUYV_to_RGB函数,重新计算索引和转换,而YUYV格式中每两个相邻像素共享U和V值 + * FIXME(20260324): 效率太慢 + */ +//bool save_YUYV_as_image(uint8_t* frameData, int width, int height, +// const char* filename, int frame_num = 0) { +// try { + // 创建CImg图像对象 (width, height, depth=1, spectrum=3 for RGB) +// CImg img(width, height, 1, 3); + + // 转换为RGB并填充到CImg +// for (int y = 0; y < height; y++) { +// for (int x = 0; x < width; x++) { +// uint8_t r, g, b; +// YUYV_to_RGB(frameData, width, height, x, y, r, g, b); + + // CImg存储顺序是平面:首先所有R,然后所有G,最后所有B + // 或者使用(x, y, 0, channel)访问 +// img(x, y, 0, 0) = r; // Red channel +// img(x, y, 0, 1) = g; // Green channel +// img(x, y, 0, 2) = b; // Blue channel +// } +// } + + // 生成带帧编号的文件名 +// char full_filename[256]; +// if (frame_num > 0) { +// snprintf(full_filename, sizeof(full_filename), +// "%s_frame%03d.bmp", filename, frame_num); +// } else { +// snprintf(full_filename, sizeof(full_filename), "%s.bmp", filename); +// } + + // 保存为BMP格式(也支持PNG、JPEG等) +// img.save(full_filename); +// std::cout << "Image saved as: " << full_filename << std::endl; + +// return true; +// } catch (CImgException& e) { +// std::cerr << "Error saving image: " << e.what() << std::endl; +// return false; +// } +//} + + + +/* + * 将CImg数据压缩成jpeg data + * + * FIXME: 具有性能问题 + */ +std::vector compress_to_jpeg(const CImg& image, int quality = 75) { + + // 检查图像的有效性 + if (image.width() == 0 || image.height() == 0) { + std::cerr << "Error: Invalid image dimensions!" << std::endl; + return std::vector(); + } + + // 检查频谱(通道数) + int channels = image.spectrum(); + + // CImg数据布局:默认是平面格式 [plane1, plane2, plane3] + // 但我们需要交错的RGB格式用于JPEG压缩 + + unsigned char* buffer = nullptr; + unsigned long buffer_size = 0; + + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + jpeg_mem_dest(&cinfo, &buffer, &buffer_size); + + int width = image.width(); + int height = image.height(); + + cinfo.image_width = width; + cinfo.image_height = height; + cinfo.input_components = 3; // 总是使用3通道RGB + cinfo.in_color_space = JCS_RGB; + + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + + // 开始压缩 + jpeg_start_compress(&cinfo, TRUE); + + JSAMPROW row_pointer[1]; + std::vector row_buffer(width * 3); // 用于存储一行交错的RGB数据 + + // 处理每一行 + for (int y = 0; y < height; y++) { + // 将CImg的平面格式转换为交错的RGB格式 + if (channels == 3) { + // RGB图像:CImg存储为平面格式 [R平面, G平面, B平面] + for (int x = 0; x < width; x++) { + row_buffer[x * 3] = image(x, y, 0, 0); // R + row_buffer[x * 3 + 1] = image(x, y, 0, 1); // G + row_buffer[x * 3 + 2] = image(x, y, 0, 2); // B + } + } + else if (channels == 1) { + // 灰度图像:重复到三个通道 + for (int x = 0; x < width; x++) { + unsigned char gray = image(x, y, 0, 0); + row_buffer[x * 3] = gray; // R + row_buffer[x * 3 + 1] = gray; // G + row_buffer[x * 3 + 2] = gray; // B + } + } + else if (channels == 4) { + // RGBA图像:忽略Alpha通道 + for (int x = 0; x < width; x++) { + row_buffer[x * 3] = image(x, y, 0, 0); // R + row_buffer[x * 3 + 1] = image(x, y, 0, 1); // G + row_buffer[x * 3 + 2] = image(x, y, 0, 2); // B + // 忽略Alpha通道:image(x, y, 0, 3) + } + } + else { + // 其他通道数:使用前3个通道 + std::cout << "Warning: Using first 3 channels of " << channels << "-channel image" << std::endl; + for (int x = 0; x < width; x++) { + for (int c = 0; c < 3 && c < channels; c++) { + row_buffer[x * 3 + c] = image(x, y, 0, c); + } + // 如果通道数不足3,用0填充 + for (int c = channels; c < 3; c++) { + row_buffer[x * 3 + c] = 0; + } + } + } + + row_pointer[0] = row_buffer.data(); + jpeg_write_scanlines(&cinfo, row_pointer, 1); + + } + + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + + // 将JPEG数据复制到vector + std::vector jpeg_data(buffer, buffer + buffer_size); + + // 释放libjpeg分配的内存 + free(buffer); + + // 验证JPEG数据 + if (jpeg_data.size() >= 2) { + bool has_soi = (jpeg_data[0] == 0xFF && jpeg_data[1] == 0xD8); + bool has_eoi = (jpeg_data.size() >= 2 && + jpeg_data[jpeg_data.size()-2] == 0xFF && + jpeg_data[jpeg_data.size()-1] == 0xD9); + + if (!has_soi || !has_eoi) { + std::cerr << "Warning: Generated JPEG may be corrupted!" << std::endl; + } + } + + return jpeg_data; +} \ No newline at end of file diff --git a/lci/src/hd/I2C.cpp b/lci/src/hd/I2C.cpp new file mode 100644 index 0000000..d5a12ef --- /dev/null +++ b/lci/src/hd/I2C.cpp @@ -0,0 +1,79 @@ +/* + * 该文件是i2c用户界面 + * + * NOTE: 以C++的方式处理异常可能会更好 + * FIXME: 包含询问方法以确定i2c设备的状态 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "cn/中文化.hpp" + +静态的 整数 文件描述符 = -1; +static pthread_mutex_t mutex; /* FIXME: 这个锁我们只提供接口给外部使用? */ + +逻辑型 I2C_检查() { + 返回 !(文件描述符 < 0); +} + +空类型 I2C_初始化(常量 字符* 设备路径 = "/dev/i2c-1") { /* FIXME: 动态指定设备文件 */ + 如果 ((文件描述符 = open(设备路径, O_RDWR)) < 0) + 抛出异常 std::runtime_error( /* FIXME: 使用专用的异常类 */ + "Failed to open I2C bus: " + std::string(strerror(errno)) + ); +} + +空类型 I2C_清理() { + close(文件描述符); +} + +空类型 I2C_置地址(常量 uint8_t 地址) { + 如果 (ioctl(文件描述符, I2C_SLAVE, 地址) < 0) + 抛出异常 std::runtime_error( + "Failed to set I2C address: " + std::string(strerror(errno)) + ); +} + +空类型 I2C_写寄存器(无号8位整 寄存器, 无号8位整 值) { /* 写寄存器 */ + 无号8位整 缓冲区[] = {寄存器, 值}; + 如果 (write(文件描述符, 缓冲区, 2) != 2) + 抛出异常 std::runtime_error( + "I2C write error: " + std::string(strerror(errno)) + ); +} + +/* 读寄存器 */ +无号8位整 I2C_读寄存器(无号8位整 寄存器) { + + 无号8位整 值; + + 如果 (write(文件描述符, &寄存器, 1) != 1) + 抛出异常 std::runtime_error( + "I2C read error: " + std::string(strerror(errno)) + ); + + 如果 (read(文件描述符, &值, 1)!= 1) + 抛出异常 std::runtime_error( + "I2C read error: " + std::string(strerror(errno)) + ); + + 返回 值; + +} + + +/* 获取锁 */ +空类型 I2C_获取锁() { + pthread_mutex_lock(&mutex); /* 加锁 */ +} + +/* 释放锁 */ +空类型 I2C_释放锁() { + pthread_mutex_unlock(&mutex); /* 解锁 */ +} diff --git a/lci/src/hd/MPU6050.cpp b/lci/src/hd/MPU6050.cpp new file mode 100644 index 0000000..2d4121e --- /dev/null +++ b/lci/src/hd/MPU6050.cpp @@ -0,0 +1,104 @@ + +/* + * FIXME: 1. 传感器数据校准 + 2. 可采用卡尔曼滤波 + + FIXME: 采用类进行封装,因为可能需要一对 + */ +#include +#include +#include +#include +#include "cn/中文化.hpp" +#include "Dashun_linux.hpp" + +静态的 预求值的 无号8位整 MPU6050_默认地址 = 0x68; +静态的 预求值的 无号8位整 MPU6050_电源管理寄存器1 = 0x6B; +静态的 预求值的 无号8位整 MPU6050_电源管理_非睡眠模式 = 0x00; +静态的 预求值的 无号8位整 MPU6050_陀螺仪配置寄存器 = 0x1B; +静态的 预求值的 无号8位整 MPU6050_陀螺仪量程_正负500度每秒 = 0x08; +静态的 预求值的 无号8位整 MPU6050_陀螺仪横轴数据_高字节 = 0x43; /* x */ +静态的 预求值的 无号8位整 MPU6050_陀螺仪横轴数据_低字节 = 0x44; +静态的 预求值的 无号8位整 MPU6050_陀螺仪纵轴数据_高字节 = 0x45; /* y */ +静态的 预求值的 无号8位整 MPU6050_陀螺仪纵轴数据_低字节 = 0x46; +静态的 预求值的 无号8位整 MPU6050_陀螺仪竖轴数据_高字节 = 0x47; /* z */ +静态的 预求值的 无号8位整 MPU6050_陀螺仪竖轴数据_低字节 = 0x48; +静态的 预求值的 无号8位整 MPU6050_加速度计配置寄存器 = 0x1C; +静态的 预求值的 无号8位整 MPU6050_加速度计量程_正负8g = 0x10; +静态的 预求值的 无号8位整 MPU6050_加速度计横轴数据_高字节 = 0x3B; +静态的 预求值的 无号8位整 MPU6050_加速度计横轴数据_低字节 = 0x3C; +静态的 预求值的 无号8位整 MPU6050_加速度计纵轴数据_高字节 = 0x3D; +静态的 预求值的 无号8位整 MPU6050_加速度计纵轴数据_低字节 = 0x3E; +静态的 预求值的 无号8位整 MPU6050_加速度计竖轴数据_高字节 = 0x3F; +静态的 预求值的 无号8位整 MPU6050_加速度计竖轴数据_低字节 = 0x40; +静态的 逻辑型 启动标志 = 假; + +逻辑型 MPU6050_检查() { + I2C_获取锁(); + 布尔 临时 = 启动标志; + I2C_释放锁(); + 返回 临时; +} + +/* 初始化 MPU6050 */ +空类型 MPU6050_初始化() { + I2C_获取锁(); + I2C_置地址(MPU6050_默认地址); + I2C_写寄存器(MPU6050_电源管理寄存器1, MPU6050_电源管理_非睡眠模式); /* 退出睡眠 */ + I2C_写寄存器(MPU6050_陀螺仪配置寄存器, MPU6050_陀螺仪量程_正负500度每秒); /* ±500°/s */ + I2C_写寄存器(MPU6050_加速度计配置寄存器, MPU6050_加速度计量程_正负8g); /* ±8g */ + 启动标志 = 真; + I2C_释放锁(); + usleep(100000); /* 等待稳定 */ +} + +/* 读取 16 位传感器数据(带符号) */ +静态的 有号16位整 读16比特寄存器(无号8位整 寄存器高位, 无号8位整 寄存器低位) { + 返回 (I2C_读寄存器(寄存器高位) << 8) | I2C_读寄存器(寄存器低位); +} + +/* 获取原始MPU6050传感器数据 */ +MPU6050_传感器数据 MPU6050_读传感器() { + MPU6050_传感器数据 数据; + I2C_获取锁(); + I2C_置地址(MPU6050_默认地址); + 数据.加速度计_横 = 读16比特寄存器(MPU6050_加速度计横轴数据_高字节, MPU6050_加速度计横轴数据_低字节); + 数据.加速度计_纵 = 读16比特寄存器(MPU6050_加速度计纵轴数据_高字节, MPU6050_加速度计纵轴数据_低字节); + 数据.加速度计_竖 = 读16比特寄存器(MPU6050_加速度计竖轴数据_高字节, MPU6050_加速度计竖轴数据_低字节); + 数据.陀螺仪_横 = 读16比特寄存器(MPU6050_陀螺仪横轴数据_高字节, MPU6050_陀螺仪横轴数据_低字节); + 数据.陀螺仪_纵 = 读16比特寄存器(MPU6050_陀螺仪纵轴数据_高字节, MPU6050_陀螺仪纵轴数据_低字节); + 数据.陀螺仪_竖 = 读16比特寄存器(MPU6050_陀螺仪竖轴数据_高字节, MPU6050_陀螺仪竖轴数据_低字节); + I2C_释放锁(); + 返回 数据; +} + +/* FIXME: 当设备剧烈运动时(如加速度远大于重力),加速度计数据会包含运动加速度,此时角度计算可能不准确。实际应用中需结合陀螺仪数据(如互补滤波或卡尔曼滤波)来提高稳定性。 */ +/* FIXME: 适合静态测量或学习用途。如果需要实际应用,建议添加传感器融合算法 */ +空类型 MPU6050_计算角度(常量 MPU6050_传感器数据& 数据, 单精度小数& 俯仰角, 单精度小数& 翻滚角) { + /* - ±2g → 16384 LSB/g + - ±4g → 8192 LSB/g + - ±8g → 4096 LSB/g + - ±16g → 2048 LSB/g */ + 常量 单精度小数 比例 = 4096.0; /* 加速度数据转重力单位(±8g 量程 4096 LSB/g) */ + 单精度小数 横角 = 数据.加速度计_横 / 比例; + 单精度小数 纵角 = 数据.加速度计_纵 / 比例; + 单精度小数 竖角 = 数据.加速度计_竖 / 比例; + + /* 计算俯仰角(Pitch)和翻滚角(Roll) */ + 俯仰角 = atan2(-横角, sqrt(纵角 * 纵角 + 竖角 * 竖角)) * 180.0 / M_PI; + 翻滚角 = atan2(纵角, 竖角) * 180.0 / M_PI; +} + +#ifdef _TEST +/* TEST MAIN */ +整数 主函数() { + I2C_初始化(); + MPU6050_初始化(); + 判断循环 (1) { + MPU6050_传感器数据 数据 = MPU6050_读传感器(); + 单精度小数 俯仰角, 翻滚角; + MPU6050_计算角度(数据, 俯仰角, 翻滚角); + printf("俯仰角: %.2f\t翻滚角: %.2f\n"); + } +} +#endif diff --git a/lci/src/hd/PCA9685.cpp b/lci/src/hd/PCA9685.cpp new file mode 100644 index 0000000..040fdb3 --- /dev/null +++ b/lci/src/hd/PCA9685.cpp @@ -0,0 +1,57 @@ +/* PCA9685 舵机驱动板 */ +#include +#include +#include "cn/中文化.hpp" +#include "Dashun_linux.hpp" + +预求值的 无号8位整 PCA9685_地址 = 0x40; /* 7位地址 */ +静态的 逻辑型 启动标志 = 假; /* 启动标志 */ +逻辑型 PCA9685_检查() { + I2C_获取锁(); + 逻辑型 临时 = 启动标志; + I2C_释放锁(); + 返回 临时; +} +空类型 PCA9685_初始化() { + I2C_获取锁(); + I2C_置地址(PCA9685_地址); + I2C_写寄存器(0x00, 0x20); /* 开启AI(地址自增)和关闭SLEEP */ + 启动标志 = 真; + I2C_释放锁(); + usleep(5000); /* 等待振荡器启动 */ + + /* 设置PWM频率为50Hz(舵机标准周期20ms) */ + 预求值的 单精度小数 赫兹频率 = 50.0; + 单精度小数 prescale_val = 25000000.0 / (4096 * 赫兹频率) - 1; /* 25MHz主频 */ + 无号8位整 预分频值 = static_cast(prescale_val + 0.5); + + I2C_获取锁(); + I2C_写寄存器(0x00, 0x10); /* 进入SLEEP模式(允许改频率) */ + I2C_写寄存器(0xFE, 预分频值); /* 写入预分频值 */ + I2C_写寄存器(0x00, 0x20); /* 退出SLEEP */ + I2C_释放锁(); + usleep(5000); +} + +/* 设置指定通道的PWM信号(0-4095) */ +空类型 PCA9685_设置PWM(无号8位整 通道, 无号16位整 导通, 无号16位整 关断) { + 无号8位整 寄存器 = 0x06 + 4 * 通道; /* LED0 寄存器起始地址 */ + I2C_获取锁(); + I2C_置地址(PCA9685_地址); + I2C_写寄存器(寄存器, 导通 & 0xFF); + I2C_写寄存器(寄存器 + 1, 导通 >> 8); + I2C_写寄存器(寄存器 + 2, 关断 & 0xFF); + I2C_写寄存器(寄存器 + 3, 关断 >> 8); + I2C_释放锁(); +} + +/* + * 角度转PWM值(0°~180° → 0.5ms~2.5ms脉宽) + */ +空类型 PCA9685_调整角度(无号8位整 通道, 单精度小数 角度) { + 预求值的 单精度小数 最小脉冲 = 0.5; /* 0°对应0.5ms */ + 预求值的 单精度小数 最大脉冲 = 2.5; /* 180°对应2.5ms */ + 单精度小数 脉冲宽度 = 最小脉冲 + (最大脉冲 - 最小脉冲) * (角度 / 180.0); + 无号16位整 关断值 = static_cast(脉冲宽度 * 4096 / 20); /* 20ms 周期 */ + PCA9685_设置PWM(通道, 0, 关断值); /* ON=0, OFF=计算值 */ +} diff --git a/lci/src/hd/mem.cpp b/lci/src/hd/mem.cpp new file mode 100644 index 0000000..24f3c01 --- /dev/null +++ b/lci/src/hd/mem.cpp @@ -0,0 +1,21 @@ +#include "hd/mem.h" + +/* 通过SysInfo接口获取内存使用情况 失败会返回NULL */ +MemoryInfo getMemoryUsageWithSysinfo() { + MemoryInfo memInfo = {0}; + struct sysinfo si; + + if (sysinfo(&si) == 0) + { + memInfo.totalMemory = si.totalram * si.mem_unit; + memInfo.freeMemory = si.freeram * si.mem_unit; + memInfo.usedMemory = memInfo.totalMemory - memInfo.freeMemory; + + if (memInfo.totalMemory > 0) + memInfo.usageRate = 100.0 * memInfo.usedMemory / memInfo.totalMemory; + } + else + std::cerr << "sysinfo Call Failed" << std::endl; + + return memInfo; +} diff --git a/lci/src/labeled.py b/lci/src/labeled.py new file mode 100644 index 0000000..bd82f9e --- /dev/null +++ b/lci/src/labeled.py @@ -0,0 +1,590 @@ +#!/usr/bin/env python3 +""" +图片标注工具 +用于手动标注图像中的目标区域并保存位置坐标(百分比格式)到txt文件 +支持矩形框标注、预览、编辑和删除功能 +""" + +import os +import sys +import json +import tkinter as tk +from tkinter import ttk, filedialog, messagebox, simpledialog +from PIL import Image, ImageTk, ImageDraw +import cv2 +import numpy as np +from datetime import datetime +from typing import List, Optional, Tuple +import threading + + +class ImageAnnotationTool: + """图片标注工具 - 支持手动矩形框标注""" + + def __init__(self, root): + self.root = root + self.root.title("图片标注工具") + self.root.geometry("1200x800") + + # 数据存储 + self.current_image_path = None + self.current_image = None + self.image_pil = None + self.image_tk = None + self.zoom_factor = 1.0 + self.image_display = None + self.annotations = [] # 存储标注:[(x1, y1, x2, y2), ...] # 移除了label字段 + + # 标注状态 + self.drawing = False + self.start_x = None + self.start_y = None + self.current_rect = None + self.selected_annotation = -1 # 当前选中的标注索引 + self.drag_mode = False # 是否在拖拽模式 + self.drag_start = None # 拖拽起始点 + + # 缩放和平移 + self.offset_x = 0 + self.offset_y = 0 + self.pan_start_x = 0 + self.pan_start_y = 0 + self.panning = False + + # 创建UI + self.setup_ui() + + # 绑定事件 + self.setup_bindings() + + # 更新状态 + self.update_status() + + def setup_ui(self): + """设置用户界面""" + # 创建主框架 + main_frame = ttk.Frame(self.root) + main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 左侧控制面板 + control_frame = ttk.Frame(main_frame, width=250) + control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5)) + control_frame.pack_propagate(False) + + # 文件操作 + file_group = ttk.LabelFrame(control_frame, text="文件操作", padding=5) + file_group.pack(fill=tk.X, pady=(0, 10)) + + ttk.Button(file_group, text="打开图片", command=self.open_image).pack(fill=tk.X, pady=2) + ttk.Button(file_group, text="保存标注", command=self.save_annotations).pack(fill=tk.X, pady=2) + ttk.Button(file_group, text="加载标注", command=self.load_annotations).pack(fill=tk.X, pady=2) + + # 标注操作 + anno_group = ttk.LabelFrame(control_frame, text="标注操作", padding=5) + anno_group.pack(fill=tk.X, pady=(0, 10)) + + ttk.Button(anno_group, text="清除当前标注", command=self.clear_current_annotation).pack(fill=tk.X, pady=2) + ttk.Button(anno_group, text="删除所有标注", command=self.clear_all_annotations).pack(fill=tk.X, pady=2) + ttk.Button(anno_group, text="删除选中标注", command=self.delete_selected_annotation).pack(fill=tk.X, pady=2) + + # 标注列表 + list_group = ttk.LabelFrame(control_frame, text="标注列表", padding=5) + list_group.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) + + # 创建列表框和滚动条 + list_frame = ttk.Frame(list_group) + list_frame.pack(fill=tk.BOTH, expand=True) + + self.annotation_listbox = tk.Listbox(list_frame, height=15) + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.annotation_listbox.yview) + self.annotation_listbox.configure(yscrollcommand=scrollbar.set) + + self.annotation_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + self.annotation_listbox.bind('<>', self.on_annotation_select) + + # 信息显示 + info_group = ttk.LabelFrame(control_frame, text="信息", padding=5) + info_group.pack(fill=tk.X) + + self.info_label = ttk.Label(info_group, text="未加载图片", wraplength=220) + self.info_label.pack(fill=tk.X, pady=2) + + self.status_label = ttk.Label(info_group, text="状态: 就绪", wraplength=220) + self.status_label.pack(fill=tk.X, pady=2) + + # 右侧画布区域 + canvas_frame = ttk.Frame(main_frame) + canvas_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) + + # 工具栏 + toolbar = ttk.Frame(canvas_frame) + toolbar.pack(fill=tk.X, pady=(0, 5)) + + ttk.Button(toolbar, text="放大", command=self.zoom_in).pack(side=tk.LEFT, padx=2) + ttk.Button(toolbar, text="缩小", command=self.zoom_out).pack(side=tk.LEFT, padx=2) + ttk.Button(toolbar, text="适应窗口", command=self.fit_to_window).pack(side=tk.LEFT, padx=2) + ttk.Button(toolbar, text="重置视图", command=self.reset_view).pack(side=tk.LEFT, padx=2) + + self.draw_mode_var = tk.BooleanVar(value=True) + ttk.Checkbutton(toolbar, text="绘制模式", variable=self.draw_mode_var).pack(side=tk.LEFT, padx=5) + + # 画布 + self.canvas = tk.Canvas(canvas_frame, bg='gray', cursor='cross') + self.canvas.pack(fill=tk.BOTH, expand=True) + + def setup_bindings(self): + """设置事件绑定""" + self.canvas.bind('', self.on_mouse_down) + self.canvas.bind('', self.on_mouse_move) + self.canvas.bind('', self.on_mouse_up) + self.canvas.bind('', self.on_pan_start) + self.canvas.bind('', self.on_pan_move) + self.canvas.bind('', self.on_pan_end) + self.root.bind('', lambda e: self.delete_selected_annotation()) + self.root.bind('', lambda e: self.save_annotations()) + self.root.bind('', lambda e: self.open_image()) + + def open_image(self): + """打开图片文件""" + file_path = filedialog.askopenfilename( + title="选择图片文件", + filetypes=[ + ("图片文件", "*.jpg *.jpeg *.png *.bmp *.tiff"), + ("所有文件", "*.*") + ] + ) + + if not file_path: + return + + try: + # 加载图片 + self.current_image_path = file_path + self.image_pil = Image.open(file_path) + + # 重置状态 + self.annotations = [] + self.selected_annotation = -1 + self.zoom_factor = 1.0 + self.offset_x = 0 + self.offset_y = 0 + + # 适应窗口 + self.fit_to_window() + + # 更新标注列表 + self.update_annotation_list() + + # 更新信息 + self.update_info() + self.update_status("图片已加载") + + except Exception as e: + messagebox.showerror("错误", f"无法加载图片: {e}") + + def fit_to_window(self): + """调整图片适应窗口""" + if not self.image_pil: + return + + canvas_width = self.canvas.winfo_width() + canvas_height = self.canvas.winfo_height() + + if canvas_width <= 1 or canvas_height <= 1: + canvas_width = 800 + canvas_height = 600 + + img_width, img_height = self.image_pil.size + + # 计算缩放比例 + scale_x = canvas_width / img_width + scale_y = canvas_height / img_height + self.zoom_factor = min(scale_x, scale_y) + + # 计算偏移量使图片居中 + scaled_width = img_width * self.zoom_factor + scaled_height = img_height * self.zoom_factor + self.offset_x = (canvas_width - scaled_width) / 2 + self.offset_y = (canvas_height - scaled_height) / 2 + + self.display_image() + + def reset_view(self): + """重置视图""" + self.zoom_factor = 1.0 + self.offset_x = 0 + self.offset_y = 0 + self.fit_to_window() + + def zoom_in(self): + """放大""" + self.zoom_factor *= 1.2 + self.display_image() + + def zoom_out(self): + """缩小""" + self.zoom_factor /= 1.2 + self.display_image() + + def display_image(self): + """显示图片""" + if not self.image_pil: + return + + # 缩放图片 + img_width, img_height = self.image_pil.size + new_width = int(img_width * self.zoom_factor) + new_height = int(img_height * self.zoom_factor) + + scaled_image = self.image_pil.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # 转换为PhotoImage + self.image_tk = ImageTk.PhotoImage(scaled_image) + + # 清除画布 + self.canvas.delete("all") + + # 显示图片 + self.canvas.create_image(self.offset_x, self.offset_y, anchor=tk.NW, image=self.image_tk) + + # 绘制标注 + self.draw_annotations() + + def draw_annotations(self): + """绘制所有标注""" + if not self.image_pil: + return + + img_width, img_height = self.image_pil.size + + for idx, (x1, y1, x2, y2) in enumerate(self.annotations): + # 转换坐标 + canvas_x1 = x1 * self.zoom_factor + self.offset_x + canvas_y1 = y1 * self.zoom_factor + self.offset_y + canvas_x2 = x2 * self.zoom_factor + self.offset_x + canvas_y2 = y2 * self.zoom_factor + self.offset_y + + # 确定颜色 + if idx == self.selected_annotation: + color = "red" + width = 3 + else: + color = "blue" + width = 2 + + # 绘制矩形 + self.canvas.create_rectangle(canvas_x1, canvas_y1, canvas_x2, canvas_y2, + outline=color, width=width, tags=f"rect_{idx}") + + # 绘制序号 + self.canvas.create_text(canvas_x1 + 5, canvas_y1 + 15, + text=str(idx + 1), anchor=tk.NW, + fill=color, font=("Arial", 10, "bold"), + tags=f"label_{idx}") + + def canvas_to_image_coords(self, canvas_x, canvas_y): + """将画布坐标转换为图片坐标""" + img_x = (canvas_x - self.offset_x) / self.zoom_factor + img_y = (canvas_y - self.offset_y) / self.zoom_factor + return img_x, img_y + + def on_mouse_down(self, event): + """鼠标按下事件""" + if self.draw_mode_var.get(): + # 绘制模式 + img_x, img_y = self.canvas_to_image_coords(event.x, event.y) + if 0 <= img_x <= self.image_pil.size[0] and 0 <= img_y <= self.image_pil.size[1]: + self.drawing = True + self.start_x = img_x + self.start_y = img_y + else: + # 选择模式 - 查找最近的标注 + self.find_nearest_annotation(event.x, event.y) + + def find_nearest_annotation(self, canvas_x, canvas_y): + """查找最近的标注""" + if not self.annotations: + return + + min_dist = float('inf') + nearest_idx = -1 + + for idx, (x1, y1, x2, y2) in enumerate(self.annotations): + # 转换坐标 + cx1 = x1 * self.zoom_factor + self.offset_x + cy1 = y1 * self.zoom_factor + self.offset_y + cx2 = x2 * self.zoom_factor + self.offset_x + cy2 = y2 * self.zoom_factor + self.offset_y + + # 计算到矩形边界的距离 + if cx1 <= canvas_x <= cx2 and cy1 <= canvas_y <= cy2: + # 点在矩形内 + dist = 0 + else: + # 计算到最近边的距离 + dx = max(cx1 - canvas_x, 0, canvas_x - cx2) + dy = max(cy1 - canvas_y, 0, canvas_y - cy2) + dist = (dx**2 + dy**2)**0.5 + + if dist < min_dist: + min_dist = dist + nearest_idx = idx + + # 如果距离足够近,选中该标注 + if min_dist < 20: # 20像素阈值 + self.selected_annotation = nearest_idx + self.annotation_listbox.selection_clear(0, tk.END) + self.annotation_listbox.selection_set(nearest_idx) + self.annotation_listbox.see(nearest_idx) + self.display_image() + self.update_status(f"选中标注 #{nearest_idx + 1}") + + def on_mouse_move(self, event): + """鼠标移动事件""" + if self.drawing: + # 绘制临时矩形 + if self.current_rect: + self.canvas.delete(self.current_rect) + + img_x, img_y = self.canvas_to_image_coords(event.x, event.y) + canvas_x1 = self.start_x * self.zoom_factor + self.offset_x + canvas_y1 = self.start_y * self.zoom_factor + self.offset_y + canvas_x2 = img_x * self.zoom_factor + self.offset_x + canvas_y2 = img_y * self.zoom_factor + self.offset_y + + self.current_rect = self.canvas.create_rectangle( + canvas_x1, canvas_y1, canvas_x2, canvas_y2, + outline="green", width=2, dash=(5, 5) + ) + + def on_mouse_up(self, event): + """鼠标释放事件""" + if self.drawing: + # 完成绘制 + img_x, img_y = self.canvas_to_image_coords(event.x, event.y) + + # 确保坐标正确排序 + x1 = min(self.start_x, img_x) + y1 = min(self.start_y, img_y) + x2 = max(self.start_x, img_x) + y2 = max(self.start_y, img_y) + + # 检查矩形大小 + if x2 - x1 > 5 and y2 - y1 > 5: + # 添加标注(只保存坐标,不保存标签) + self.annotations.append((x1, y1, x2, y2)) + self.update_annotation_list() + self.display_image() + self.update_status(f"已添加标注 #{len(self.annotations)}") + else: + self.update_status("标注太小,已忽略") + + # 清理绘制状态 + self.drawing = False + if self.current_rect: + self.canvas.delete(self.current_rect) + self.current_rect = None + + def on_pan_start(self, event): + """开始平移""" + self.panning = True + self.pan_start_x = event.x + self.pan_start_y = event.y + + def on_pan_move(self, event): + """平移移动""" + if self.panning: + dx = event.x - self.pan_start_x + dy = event.y - self.pan_start_y + self.offset_x += dx + self.offset_y += dy + self.pan_start_x = event.x + self.pan_start_y = event.y + self.display_image() + + def on_pan_end(self, event): + """结束平移""" + self.panning = False + + def update_annotation_list(self): + """更新标注列表""" + self.annotation_listbox.delete(0, tk.END) + for idx, (x1, y1, x2, y2) in enumerate(self.annotations): + width = x2 - x1 + height = y2 - y1 + list_text = f"{idx + 1}. 矩形 ({width:.0f}x{height:.0f})" + self.annotation_listbox.insert(tk.END, list_text) + + def on_annotation_select(self, event): + """标注列表选择事件""" + selection = self.annotation_listbox.curselection() + if selection: + self.selected_annotation = selection[0] + self.display_image() + self.update_status(f"已选中标注 #{self.selected_annotation + 1}") + + def delete_selected_annotation(self): + """删除选中的标注""" + if self.selected_annotation >= 0 and self.selected_annotation < len(self.annotations): + del self.annotations[self.selected_annotation] + self.selected_annotation = -1 + self.update_annotation_list() + self.display_image() + self.update_status("已删除选中标注") + + def clear_current_annotation(self): + """清除当前标注""" + self.selected_annotation = -1 + self.display_image() + self.update_status("已清除选中状态") + + def clear_all_annotations(self): + """删除所有标注""" + if messagebox.askyesno("确认", "确定要删除所有标注吗?"): + self.annotations = [] + self.selected_annotation = -1 + self.update_annotation_list() + self.display_image() + self.update_status("已删除所有标注") + + def save_annotations(self): + """保存标注到文件(仅TXT格式,纯坐标,格式:top right bottom left)""" + if not self.current_image_path: + messagebox.showwarning("警告", "请先加载图片") + return + + if not self.annotations: + if not messagebox.askyesno("确认", "没有标注要保存,是否继续?"): + return + + try: + # 获取图像尺寸 + img_width, img_height = self.image_pil.size + + # 生成保存路径:图片文件名.txt(同目录下) + image_dir = os.path.dirname(self.current_image_path) + image_basename = os.path.splitext(os.path.basename(self.current_image_path))[0] + file_path = os.path.join(image_dir, f"{image_basename}.txt") + + # 保存标注(纯坐标格式,top right bottom left) + with open(file_path, 'w', encoding='utf-8') as f: + for x1, y1, x2, y2 in self.annotations: + # 转换为百分比坐标(top, right, bottom, left) + top_pct = y1 / img_height + right_pct = x2 / img_width + bottom_pct = y2 / img_height + left_pct = x1 / img_width + + # 写入坐标数据(保留6位小数,空格分隔) + f.write(f"{top_pct:.6f} {right_pct:.6f} {bottom_pct:.6f} {left_pct:.6f}\n") + + self.update_status(f"已保存 {len(self.annotations)} 个标注到 {os.path.basename(file_path)}") + messagebox.showinfo("成功", f"标注已保存到:\n{file_path}\n\n格式: top right bottom left (百分比坐标)") + + except Exception as e: + messagebox.showerror("错误", f"保存失败: {e}") + + def load_annotations(self): + """加载标注文件(TXT格式,纯坐标,格式:top right bottom left)""" + if not self.current_image_path: + messagebox.showwarning("警告", "请先加载图片") + return + + # 默认加载同目录下的同名txt文件 + image_dir = os.path.dirname(self.current_image_path) + image_basename = os.path.splitext(os.path.basename(self.current_image_path))[0] + default_file = os.path.join(image_dir, f"{image_basename}.txt") + + if os.path.exists(default_file): + file_path = default_file + else: + file_path = filedialog.askopenfilename( + title="选择标注文件", + initialdir=image_dir, + filetypes=[ + ("文本文件", "*.txt"), + ("所有文件", "*.*") + ] + ) + + if not file_path: + return + + try: + self.load_txt_annotations(file_path) + self.update_annotation_list() + self.display_image() + self.update_status(f"已加载 {len(self.annotations)} 个标注") + + except Exception as e: + messagebox.showerror("错误", f"加载失败: {e}") + + def load_txt_annotations(self, file_path): + """加载TXT格式的标注文件(纯坐标格式,top right bottom left)""" + self.annotations = [] + img_width, img_height = self.image_pil.size + + with open(file_path, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + line = line.strip() + if not line: + continue + + parts = line.split() + if len(parts) == 4: + try: + top_pct = float(parts[0]) + right_pct = float(parts[1]) + bottom_pct = float(parts[2]) + left_pct = float(parts[3]) + + # 验证百分比坐标范围 + if all(0 <= v <= 1 for v in [top_pct, right_pct, bottom_pct, left_pct]): + # 转换为绝对坐标 + x1 = left_pct * img_width + y1 = top_pct * img_height + x2 = right_pct * img_width + y2 = bottom_pct * img_height + + self.annotations.append((x1, y1, x2, y2)) + else: + print(f"警告: 第{line_num}行坐标超出范围 [0,1]: {line}") + except ValueError as e: + print(f"警告: 第{line_num}行格式错误: {line}, 错误: {e}") + else: + print(f"警告: 第{line_num}行格式错误,需要4个坐标值: {line}") + + def update_info(self): + """更新信息显示""" + if self.current_image_path and self.image_pil: + img_width, img_height = self.image_pil.size + filename = os.path.basename(self.current_image_path) + self.info_label.config(text=f"图片: {filename}\n尺寸: {img_width}x{img_height}\n标注: {len(self.annotations)}个") + else: + self.info_label.config(text="未加载图片") + + def update_status(self, message=None): + """更新状态栏""" + if message: + self.status_label.config(text=f"状态: {message}") + else: + self.status_label.config(text="状态: 就绪") + + def update(self): + """更新界面""" + self.update_info() + self.root.after(1000, self.update) + + +def main(): + """主函数""" + root = tk.Tk() + app = ImageAnnotationTool(root) + app.update() + root.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lci/src/main.cpp b/lci/src/main.cpp new file mode 100644 index 0000000..ec7fa75 --- /dev/null +++ b/lci/src/main.cpp @@ -0,0 +1,1118 @@ +/* =============================================== + * 通用子系统、进程机器、进程托管服务 + * + * + * 【主函数文件】 + * + * 直接子系统调用,或者启动一个pmc子系统。 + * =============================================== + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* 注册信号以优雅退出 */ +#include +#include +#include +#include +#include +#include +#include /* 解析命令行参数 */ +#include +#include +#include "boost/property_tree/json_parser.hpp" +#include "net/HttpServer.hpp" +#include "logs/Logger.hpp" +#include "th/Thread.hpp" +#include "th/Cv_wait.hpp" +#include "th/ITask.hpp" +#include "tt/Mq.h" +#include "tt/Pipe.h" + +/* 中文模式 Chinese Mode */ +#include "cn/中文化.hpp" + +/* 静态区 - 存放全局变量 */ +static const char *PMC_VERSION = "0.0.8"; /* 版本号 */ +static const size_t MSGLEN = 2048; /* 单条消息的长度 */ +static const size_t MSGCNT = 100; /* 消息条数 */ + +using namespace qing; +namespace po = boost::program_options; + + +void pmc_init(po::variables_map&); /* 初始化pmc并行机器 */ +void pmc_serve(po::variables_map&); /* pmc的http伺服器模式 */ +void parse_self_init_list(std::string&); /* 解析自启动列表文件 */ +void parse_data_from_mq(std::string&); /* 解析消息队列收到的消息 */ +std::string send_start_and_recv_pipe(const std::string& name_mq, const std::string& exec); /* 发送启动命令并且接收回复 */ +std::string send_list_and_recv_pipe(const std::string& name_mq); /* 发送启动命令并且接收回复 */ +std::string send_kill_and_recv_pipe(const std::string& name_mq, const int kill); /* 发送删除命令并且接收回复 */ + + +//------------------------------------------------------------- +//------------------------------------------------------------- +namespace qing { + +/* 子进程任务 */ +class ProcessTask: public ITask { +public: + + /* 输入一个字符串(要执行的命令),构造子进程任务(但是并没有执行起来) */ + ProcessTask(std::string cmd): cmd(cmd) {} + + /* 任务是否在运行中 */ + bool isRunning() override { return p && p->running(); } + + /* 启动任务 */ + void start() override { + if (!isRunning()) this->p = std::make_unique(cmd); } + + /* 终止该任务(并且重置子进程指针) */ + void stop() override { + if (isRunning()) { + p->terminate(); + } + p.reset(); + } + + /* 获取该任务(子进程)的命令 */ + std::string check() { return this->cmd; } + + /* 获取该子进程的pid(进程编号) */ + pid_t pid() { return p->id(); } + +private: + std::unique_ptr p; /* 子进程指针 */ + std::string cmd; /* 该进程执行的命令 */ +}; + +} + + +//------------------------------------------------------------- +//------------------------------------------------------------- +namespace qing { + + +/* + * TRANSMIT TASK 通信任务 + * (创建一个通信伺服器,它收到消息就执行回调) + */ +class Tttask: public ITask { +public: + +/* msglen: 单条消息的长度 + * msgcnt: 消息伺服长度 + * callback: 回调函数 */ +Tttask(const size_t msglen, size_t msgcnt, const sf_t callback) +: msglen(msglen), msgcnt(msgcnt) { + this->callback = std::make_unique(callback); + init_thread(); +} + +/* 启动该通信伺服任务 */ +void start() override { + if (!this->isRunning()){ + th->Activate(); + th->WaitStart(); /* TODO: add timeout 增加超时 */ + } +} + +/* 终止这个通信伺服任务 */ +void stop() override { th->WaitClose(); } + +/* 该任务是否在运行 + * + * FIXME: 对START状态的处理还应该考虑 */ +bool isRunning() override { + auto stat = th->check(); + return !(stat == Fsm::Stat::STOP + || stat == Fsm::Stat::SHUT); +} + + +private: +std::unique_ptr th; /* 可控线程指针 */ +std::unique_ptr callback; /* 回调行为(接收之后)指针 */ +std::unique_ptr mq; /* 消息队列指针 */ +size_t msglen=0, msgcnt=0; /* 消息的缓存规格 */ +std::string name; /* 消息队列名(进程号) */ + + + + +/* 获取当前进程编号,用于设置消息队列的名字 */ +std::string get_pid_str() { return std::to_string(getpid()); } + +/* 初始化通信伺服线程 */ +void init_thread() { + + + this->th = std::make_unique ( + + /* stop callback 静止事件 */ + [](Thread& th) -> void { + th.suspend(); + }, + + /* start callback 唤醒事件 */ + [this](Thread& th) -> void { + name = get_pid_str(); + mq = std::make_unique (name, msglen, msgcnt, Mq::CREATOR); + th.run(); + }, + + /* loop callback 循环事件 */ + [this](Thread& th) -> void { + + /* TODO: 作为参数传入延时 */ + mq->recv(*callback, []() { usleep(10000); }); + }, + + /* clean callback 清理事件 */ + [this] (Thread& th) -> void { + mq.reset(); + } + + ); + + +} + + + +}; +} + + +/*-------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------*/ +Cv_wait cv = Cv_wait(); /* 条件变量的等待机制(让主程序等待中断信号) */ + +auto taskPool = std::vector>{}; /* 底层任务池 - !!! 非子进程 !!! */ + + + +namespace qing { + + + +/* process pool 进程池 - 这个池子只放子进程 */ +class PPool { +public: + + + /* CREATE PROCESS - 创建进程 + * + * cmd: 启动命令 */ + void crtp(const std::string& cmd) try { + auto pt = std::make_shared(cmd); + pt->start(); + pool.push_back(pt); + } + + catch (boost::process::process_error &exp) { + std::cerr << "Failed to execute this command: " << cmd << std::endl; + } + + + /* CLEAR 清理 - 删除所有已经停下的进程 */ + void clr() { + auto it = --pool.end(); + for (; it != pool.begin(); --it) + if (!(*it)-> isRunning()) { + auto tmp= it; + it++; + pool.erase(tmp); /* 自动释放 */ + } + + if (pool.size() > 0 && !(*it)->isRunning()) + pool.erase(it); /* 如果end() == begin(),此步将会试图删除哨兵 */ + } + + + /* 获取进程池的大小 */ + size_t size() { return pool.size(); } + + + /* 删除一个进程,需要输入进程的下标。 FIXME: 似乎不太自然 */ + void kill(int idx) { + auto it = pool.begin(); + int i = 0; + for (; i < idx && it != pool.end(); ++i, ++it); + + /* 如果找到了目标(计数器没有指向尾节点) */ + if (it != pool.end()) { + (*it)->stop(); + pool.erase(it); + } + } + + /* 取下标运算符重载 - 取元素对象 */ + ProcessTask& operator[](const size_t idx) { + auto it = pool.begin(); + for (int i = 0; i < idx; ++it, ++i) + if (it == pool.end()) + throw std::out_of_range("PPool::operator[]"); + return **it; + } + + /* TODO: begin()、end() + * ...... */ + +private: + + /* 进程任务的容器 */ + std::list> pool{}; + +}; +} + +PPool ppool; /* 用于存放子进程的进程池 */ + + +/*-------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------*/ +namespace qing { + + + +/* 只用于该处的pmc子进程操作 */ +namespace pmc_mtd { + +/* 列举出所有子进程,包含序号、pid、执行命令。 + * + * { + * "pipe": "", + * "type": "list", + * } + * + */ +static auto list() -> std::string { + boost::json::array processes; + + for (int i = 0; i < ppool.size(); ++i) { + processes.push_back({ + {"index", i}, + {"pid", ppool[i].pid()}, + {"status", ppool[i].check()} + }); + } + + return boost::json::serialize(processes); +} + +/* 删除一个子进程,需要提供pid + * + * { + * "pipe": "", + * "type": "kill", + * "kill": + * } + * + */ +static auto kill(const int pid) -> bool { + for (int i=0; i", + * "type": "exec", + * "exec": "" + * } + * + */ +static auto exec(const std::string& cmd) -> bool try { + ppool.clr(); + ppool.crtp(cmd); + return true; +} + +catch(std::exception& exp) { + return false; +} + + +} +} + + + +/*==============================================================*/ +/* 这些是尚未整理的工具 */ + +/* json字符串转boost.ptree + * FIXME: 这个方法好像在ProcessManagerService进程托管服务中集成了*/ +boost::property_tree::ptree parseJsonToPtree(const std::string& jsonStr) { + boost::property_tree::ptree pt; + try { + std::stringstream ss(jsonStr); + boost::property_tree::read_json(ss, pt); + } catch (const boost::property_tree::json_parser_error& e) { + // 解析失败处理 + std::cerr << "JSON parse error: " << e.what() << std::endl; + } + return pt; +} + +/* url 解码 */ +static std::string url_decode(const std::string& str) { + std::string result; + std::istringstream iss(str); + char ch; + + while (iss.get(ch)) { + if (ch == '%') { + int hex; + if (iss >> std::hex >> hex) { + result += static_cast(hex); + } + } else if (ch == '+') { + result += ' '; + } else { + result += ch; + } + } + return result; +} + + +/* ============================================================== */ +namespace qing { +/** + * @brief 进程托管服务类 + * + * 提供进程的列表、执行和终止功能 + */ +class ProcessManagerService { +public: + /** + * @brief 构造函数 + * @param server HTTP服务器引用 + */ + explicit ProcessManagerService(pmc::net::HttpServer& server); + + /** + * @brief 注册所有进程管理相关的路由 + */ + void registerRoutes(); + +private: + pmc::net::HttpServer& server_; + + /** + * @brief 解析请求体中的JSON数据 + * @param body 请求体字符串 + * @return JSON解析后的属性树 + */ + boost::property_tree::ptree parseJsonBody(const std::string& body); + + /** + * @brief 构建JSON响应 + * @param success 是否成功 + * @param message 消息 + * @param data 附加数据 + * @return JSON字符串 + */ + std::string buildJsonResponse(bool success, const std::string& message, + const boost::property_tree::ptree& data = boost::property_tree::ptree()); + + /** + * @brief 从请求中提取target参数 + * @param params 查询参数 + * @return target值(空字符串表示本机) + */ + std::string getTarget(const std::unordered_map& params); + + /** + * @brief 从请求中提取exec参数 + * @param params 查询参数 + * @return 要执行的指令 + */ + std::string getExecCommand(const std::unordered_map& params); + + /** + * @brief 从请求中提取kill参数 + * @param params 查询参数 + * @return 目标进程号 + */ + int getPid(const std::unordered_map& params); + + // POST方法处理器 + http::response handleList( + const http::request& req, + const std::unordered_map& params); + + http::response handleExec( + const http::request& req, + const std::unordered_map& params); + + http::response handleKill( + const http::request& req, + const std::unordered_map& params); + + // 内部实现函数(留空) + std::string listProcesses(const std::string& target); + std::string execCommand(const std::string& command, const std::string& target); + bool killProcess(int pid, const std::string& target); +}; + + +ProcessManagerService::ProcessManagerService(pmc::net::HttpServer& server) + : server_(server) { +} + +void ProcessManagerService::registerRoutes() { + // 注册POST路由 + server_.post("/list", [this](const auto& req, const auto& params) { + return this->handleList(req, params); + }); + + server_.post("/exec", [this](const auto& req, const auto& params) { + return this->handleExec(req, params); + }); + + server_.post("/kill", [this](const auto& req, const auto& params) { + return this->handleKill(req, params); + }); +} + +boost::property_tree::ptree ProcessManagerService::parseJsonBody(const std::string& body) { + boost::property_tree::ptree pt; + try { + std::stringstream ss(body); + boost::property_tree::read_json(ss, pt); + } catch (...) { + // JSON解析失败,返回空树 + } + return pt; +} + +std::string ProcessManagerService::buildJsonResponse(bool success, const std::string& message, + const boost::property_tree::ptree& data) { + boost::property_tree::ptree response; + response.put("success", success); + response.put("message", message); + if (!data.empty()) { + response.add_child("data", data); + } + + std::stringstream ss; + boost::property_tree::write_json(ss, response); + return ss.str(); +} + +std::string ProcessManagerService::getTarget(const std::unordered_map& params) { + auto it = params.find("target"); + if (it != params.end()) { + return url_decode(it->second); // <--- 这里 + } + return ""; +} + +std::string ProcessManagerService::getExecCommand(const std::unordered_map& params) { + auto it = params.find("command"); + if (it != params.end()) { + return url_decode(it->second); // <--- 这里 + } + return ""; +} + +int ProcessManagerService::getPid(const std::unordered_map& params) { + auto it = params.find("pid"); + if (it != params.end()) { + std::string decoded = url_decode(it->second); // <--- 这里 + return std::stoi(decoded); + } + return -1; +} + +http::response ProcessManagerService::handleList( + const http::request& req, + const std::unordered_map& params) { + + http::response res; + res.version(req.version()); + res.set(http::field::content_type, "application/json"); + + try { + std::string target = getTarget(params); + + std::string result = listProcesses(target); + boost::property_tree::ptree data = parseJsonToPtree(result); + + res.result(http::status::ok); + res.body() = buildJsonResponse(true, "Process list retrieved successfully", data); + } catch (const std::exception& e) { + res.result(http::status::internal_server_error); + res.body() = buildJsonResponse(false, std::string("Error: ") + e.what()); + } + + res.prepare_payload(); + return res; +} + +http::response ProcessManagerService::handleExec( + const http::request& req, + const std::unordered_map& params) { + + http::response res; + res.version(req.version()); + res.set(http::field::content_type, "application/json"); + + try { + std::string target = getTarget(params); + std::string command = getExecCommand(params); + + if (command.empty()) { + // 尝试从POST body中获取command + auto body_json = parseJsonBody(req.body()); + command = body_json.get("command", ""); + } + + if (command.empty()) { + res.result(http::status::bad_request); + res.body() = buildJsonResponse(false, "Missing required parameter: command"); + res.prepare_payload(); + return res; + } + + // TODO: 实现命令执行逻辑 + std::string result = execCommand(command, target); + + boost::property_tree::ptree data; + data.put("command", command); + data.put("output", result); + + res.result(http::status::ok); + res.body() = buildJsonResponse(true, "Command executed successfully", data); + } catch (const std::exception& e) { + res.result(http::status::internal_server_error); + res.body() = buildJsonResponse(false, std::string("Error: ") + e.what()); + } + + res.prepare_payload(); + return res; +} + +http::response ProcessManagerService::handleKill( + const http::request& req, + const std::unordered_map& params) { + + http::response res; + res.version(req.version()); + res.set(http::field::content_type, "application/json"); + + try { + std::string target = getTarget(params); + int pid = getPid(params); + + if (pid <= 0 && !req.body().empty()) { + // 尝试从POST body中获取pid + auto body_json = parseJsonBody(req.body()); + pid = body_json.get("pid", -1); + } + + if (pid <= 0) { + res.result(http::status::bad_request); + res.body() = buildJsonResponse(false, "Missing or invalid parameter: pid"); + res.prepare_payload(); + return res; + } + + bool success = killProcess(pid, target); + + if (success) { + boost::property_tree::ptree data; + data.put("pid", pid); + res.result(http::status::ok); + res.body() = buildJsonResponse(true, "Process terminated successfully", data); + } else { + res.result(http::status::internal_server_error); + res.body() = buildJsonResponse(false, "Failed to terminate process"); + } + } catch (const std::exception& e) { + res.result(http::status::internal_server_error); + res.body() = buildJsonResponse(false, std::string("Error: ") + e.what()); + } + + res.prepare_payload(); + return res; +} + +// 如果target为空,在本机执行 +// 否则在目标服务器执行 +std::string ProcessManagerService::listProcesses(const std::string& target) +{ + if (target == "") + return pmc_mtd::list(); + else { + return send_list_and_recv_pipe(target); + } +} + +std::string ProcessManagerService::execCommand(const std::string& command, const std::string& target) +{ + if (target == "") + return pmc_mtd::exec(command) ? "OK" :"ERR"; + else { + return send_start_and_recv_pipe(target, command); /* 发送启动命令并且接收回复 */ + } +} + +bool ProcessManagerService::killProcess(int pid, const std::string& target) +{ + if (target == "") + return pmc_mtd::kill(pid); + else + return send_kill_and_recv_pipe(target, pid) == "OK"; +} +} + + +std::unique_ptr server; +/*-------------------------------------------------------------------------*/ + + +/* 捕获中断信号,结束线程并且退出 */ +void signalHandler(int signum) +{ + LOG_INFO("收到终止信号,准备停止服务器..."); + cv.WakeCv(); /* 唤醒条件信号即关闭程序 */ + if (server) + server->stop(); /* TODO: 封装成ITask */ +} + +/*---------------- SIGNAL ACTION ----------------*/ +// +/* 对信号的响应行为 TODO: 我应该系统地学习一下这个库 */ +void set_signal_action() { + + /* 该结构体用于描述信号的处理方式 */ + struct sigaction sa; + + + /* 绑定处理函数 */ + sa.sa_handler = signalHandler; + + /* + * 清空并初始化一个信号掩码集 --------------> 用于指定哪些信号在当前或即将执行的信号处理函数期间应该被阻塞(位掩码) + * “在执行这个信号处理函数时,除了当前正在处理的信号外,还希望临时阻塞其他某些信号” + * 不阻塞其他信号 + * 确保在执行期间没有其他特定的信号会被阻塞 + * 防止处理过程中被其他信号中断 + */ + sigemptyset(&sa.sa_mask); + + /* + * 用于设置信号处理函数的行为特性。 + * 0 - 默认选项 + * SA_NODEFER - 表示在执行信号处理函数时不允许其他信号被阻塞 + * SA_RESETHAND - 在信号处理函数返回后,将信号的处理方式重置为默认 + * SA_NOCLDSTOP - 对于子进程发送的停止信号,父进程不会停止。 + * SA_NOCLDWAIT - 父进程在等待等待子进程终止时不会因为子进程发送信号而收到通知 + * SA_SIGINFO - 使用扩展的信号信息,信号处理函数接收三个参数而不是一个 + */ + sa.sa_flags = 0; + + /* 注册信号 */ + sigaction(SIGINT, &sa, nullptr); /* Ctrl + C */ + sigaction(SIGTERM, &sa, nullptr); /* kill 或 systemctl stop */ + +} + +/*-------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------*/ +/* 入口函数 */ +int main(int argc, char** argv) { + + /*------------------------------*/ + /* 设置信号行为*/ + set_signal_action(); + + /*------------------------------*/ + /* 解析命令行参数 */ + po::options_description desc("pmc subsystem start options"); + desc.add_options() /* 定义选项 */ + ("help,h", "display help message") /* 帮助菜单 */ + ("version,v", "display version message") /* 显示版本 */ + /******** 子系统调用模式 *********/ + ("send,s", po::value(), "send a message to pmc sys") /* 输入目标pid */ + ("exec", po::value(), "message body") /* 启动一个进程 */ + ("list", "list process all run") /* 列出运行中的进程 */ + ("kill", po::value(), "kill a process") /* 杀死一个进程 */ + /********* 子系统服务模式 ********/ + ("init,i", po::value(), "self-init list") /* 输入自启动列表 */ + ("serve", po::value(), "Run as http server"); /* 启用http伺服器 */ + + /* 参数变量映射关系 */ + po::variables_map vm; + + try { /* 开始解析 */ + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); + if (vm.count("help")) { /* 获取帮助页面 */ + std::cout << desc << std::endl; + return 0; + } + if (vm.count("version")) { /* 获取版本信息 */ + std::cout << PMC_VERSION << std::endl; + return 0; + } + } + + catch (const std::exception& e) { + std::cerr << "ERROR: " << e.what() << std::endl; + std::cerr << "using --help to check options message" << std::endl; + throw e; + } + + /*----------------------------------------*/ + /* 初始化OpenSSL */ + //OpenSSL_add_all_algorithms(); + //ERR_load_crypto_strings(); + + if ( vm.count("send") && vm.count("exec")) { + auto send = vm["send"].as(); + auto exec = vm["exec"].as (); + auto msg = send_start_and_recv_pipe(send, exec); + std::cout << msg << std::endl; + return 0; + } + + else if ( vm.count("send") && vm.count("list")){ + auto send = vm["send"].as (); + auto msg = send_list_and_recv_pipe(send); + std::cout << msg << std::endl; + return 0; + } + + else if (vm.count("send") && vm.count("kill")) { + auto send = vm["send"].as (); + auto kill = vm["kill"].as(); + auto msg = send_kill_and_recv_pipe(send, kill); + std::cout << msg << std::endl; + return 0; + } + + /* 初始化日志模块 */ + Logger::getInstance().init(LogLevel::DEBUG, true); + + + /* 列表自启动 */ + if (vm.count("init")){ + std::string path = vm["init"].as(); + parse_self_init_list(path); + } + + + /**********************************************/ + + /* TODO: 抽象出来 */ + if (vm.count("serve")) + pmc_serve(vm); + + else if (!vm.count("serve")) { + /* pmc初始化函数 */ + pmc_init(vm); + + /* 清理 OpenSSL 资源 */ + //EVP_cleanup(); + //ERR_free_strings(); + + + /* + * 等待 + * + * 触发器在信号处理那里 + */ + cv.Wait(); + + /* 主线程被唤醒 */ + for (auto& task: taskPool) { + task->stop(); + } + } +} + + + + +/*-----------------------------------------------------------------------------*/ +/* 初始化子系统 */ +void pmc_init(po::variables_map& vm) { + + /*-------------------*/ + auto ttt = std::make_shared (MSGLEN, MSGCNT, + [](const char *data) { /* 处理收到的数据 */ + auto json_str = std::string(data); + parse_data_from_mq(json_str); + }); + ttt->start(); + taskPool.push_back(ttt); +} + +/* http伺服器模式(只监听本地)TODO: 集成为ITask */ +void pmc_serve(po::variables_map& vm) +{ + int port = vm["serve"].as(); + server = std::make_unique("127.0.0.1", port, 4); + ProcessManagerService pmService(*server); + pmService.registerRoutes(); + server->start(); + + std::cout << "Process Manager Service running on port " << port << std::endl; + std::cout << "Available endpoints:" << std::endl; + std::cout << " POST /list?target=optional" << std::endl; + std::cout << " POST /exec?command=ls&target=optional" << std::endl; + std::cout << " POST /kill?pid=1234&target=optional" << std::endl; + + server->run(); +} + +/* 解析自启动列表、添加自启动项 */ +void parse_self_init_list(std::string &path) +try { + + std::ifstream file(path); + if (!file.is_open()) { + std::cerr << "Failed to open self-init list" << std::endl; + return; + } + + std::string row = ""; + char ch; + while (file.get(ch)) + + if (ch == '\n') { + + auto pos = row.find("#"); + if (pos != -1) + row = row.substr(0, pos); + if (row != "") { + ppool.clr(); + ppool.crtp(row); + row = ""; + } + } + + else row += ch; + +} + +catch (std::exception &exp) { + std::cerr << "Exception at: parse_self_init_list()" << std::endl; + std::cerr << "Exception type: " << typeid(exp).name() << std::endl; + std::cerr << "Exception info: " << exp.what() << std::endl; +} + + +/* 解析从消息队列中接收到的数据,运行在服务端 */ +void parse_data_from_mq(std::string& json_str) +try { + boost::json::value jv = boost::json::parse(json_str); + std::string namepipe = jv.at("pipe").as_string().c_str(); + std::string typ = jv.at("type").as_string().c_str(); + auto pipe = Pipe(namepipe, Pipe::USER); + pipe.openForWrite(); + + + if (typ == "list") { + auto msg = pmc_mtd::list(); + pipe.writePipe(msg); + return; + } + + else if (typ == "kill") { + int trg = jv.at("kill").as_int64(); + auto flag = pmc_mtd::kill(trg); + std::string msg = flag ? "OK" : "Nope"; + pipe.writePipe(msg); + return; + } + + else if (typ == "exec"){ + std::string cmd = jv.at("exec").as_string().c_str(); + std::string msg = (pmc_mtd::exec(cmd)) ? "OK" : "ERR"; + pipe.writePipe(msg); + return; + } +} + +catch (std::exception &exp) { + std::cerr << "Exception at: parse_data_from_mq()" << std::endl; + std::cerr << "Exception type: " << typeid(exp).name() << std::endl; + std::cerr << "Exception info: " << exp.what() << std::endl; +} + + + + +/*-----------------------------------------------------------------------------*/ +/*-----------------------------------------------------------------------------*/ +/* 子系统调用 TODO: 使用非boost.variables_map参数 */ + + +/* 向pmc的消息队列发送启动命令并且从管道接收回复 */ +std::string send_start_and_recv_pipe(const std::string& name_mq, const std::string& exec) +try { + + auto name_pipe = std::to_string(getpid()); + auto mq = Mq(name_mq, 2048, 100, Mq::USER); /* 放在这里避免管道文件的残留 */ + + boost::json::object obj; + obj["pipe"] = name_pipe; + obj["type"] = "exec"; + obj["exec"] = exec; + auto jsonstr = boost::json::serialize(obj); + + + auto pipe = Pipe(name_pipe, Pipe::CREATOR); /* FIXME: 创建管道文件到/tmp下面 */ + if (!pipe.openForRead(true)) { + std::cerr << "Failed to open pipe" << std::endl; + return "ERR"; + } + + mq.send(jsonstr); + if (!pipe.waitForRead(1, 0)) { + std::cerr << "Pipe Read Timeout" << std::endl; + return "ERR"; + } + + std::string res = ""; + if (pipe.readPipe(&res) < 0) { + std::cerr << "Failed to read pipe" << std::endl; + return "ERR"; + } + + return res; +} + +//catch (boost::interprocess::interprocess_exception &exp) { +// std::cerr << "Target Pmc Is Not Found" << std::endl; +// return 1; +//} + +catch (std::exception &exp) { + std::cerr << "Exception at: send_start_and_recv_pipe()" << std::endl; + std::cerr << "Exception type: " << typeid(exp).name() << std::endl; + std::cerr << "Exception info: " << exp.what() << std::endl; + return "ERR"; +} + + + + +/* 向目标子系统发送列举命令并收集结果 */ +std::string send_list_and_recv_pipe(const std::string& name_mq) +try { + + auto name_pipe = std::to_string(getpid()); + auto mq = Mq(name_mq, 2048, 100, Mq::USER); + + boost::json::object obj; + obj["pipe"] = name_pipe; + obj["type"] = "list"; + auto jsonstr = boost::json::serialize(obj); + auto pipe = Pipe(name_pipe, Pipe::CREATOR); + + if (!pipe.openForRead(true)) { + std::cerr << "Failed to open pipe" << std::endl; + return "ERR"; + } + + + mq.send(jsonstr); + if (!pipe.waitForRead(1, 0)) { + std::cerr << "Pipe Read Timeout" << std::endl; + return "ERR"; + } + + std::string res = ""; + if (pipe.readPipe(&res) < 0) { + std::cerr << "Failed to read pipe" << std::endl; + return "ERR"; + } + + + return res; + +} + +//catch (boost::interprocess::interprocess_exception &exp) { +// std::cerr << "Target Pmc Is Not Found" << std::endl; +// return 1; +//} + +catch (std::exception &exp) { + std::cerr << "Exception at: send_list_and_recv_pipe()" << std::endl; + std::cerr << "Exception type: " << typeid(exp).name() << std::endl; + std::cerr << "Exception info: " << exp.what() << std::endl; + return "ERR"; +} + + + +/* 删除操作调用 */ +std::string send_kill_and_recv_pipe(const std::string& name_mq, const int kill) +try{ + + auto name_pipe = std::to_string(getpid()); + auto mq = Mq(name_mq, 2048, 100, Mq::USER); + + boost::json::object obj; + obj["pipe"] = name_pipe; + obj["type"] = "kill"; + obj["kill"] = kill; + auto jsonstr = boost::json::serialize(obj); + + auto pipe = Pipe(name_pipe, Pipe::CREATOR); + if (!pipe.openForRead(true)) { + std::cerr << "Failed to open pipe" << std::endl; + return "ERR"; + } + + mq.send(jsonstr); + if (!pipe.waitForRead(1, 0)) { + std::cerr << "Pipe Read Timeout" << std::endl; + return "ERR"; + } + + std::string res = ""; + if (pipe.readPipe(&res) < 0) { + std::cerr << "Failed to read pipe" << std::endl; + return "ERR"; + } + else return res; + +} + +/* 没有打开目标子系统的消息队列时,会触发这个异常 + * 但是实际上这里捕获的是进程间通信通用异常 */ +//catch (boost::interprocess::interprocess_exception &exp) { +// std::cerr << "Target Pmc Is Not Found" << std::endl; +// return 1; +//} + +/* 这些函数抛出异常后可能导致管道文件残留 */ +catch (std::exception &exp) { + std::cerr << "Exception at: send_kill_and_recv_pipe()" << std::endl; + std::cerr << "Exception type: " << typeid(exp).name() << std::endl; + std::cerr << "Exception info: " << exp.what() << std::endl; + return "ERR"; +} diff --git a/lci/src/mainAgent.cpp b/lci/src/mainAgent.cpp new file mode 100644 index 0000000..0daf605 --- /dev/null +++ b/lci/src/mainAgent.cpp @@ -0,0 +1,102 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "net/HttpClient.hpp" +#include "net/WebSocketServer.hpp" +#include "llm/LLMClient.hpp" +#include "llm/LLMWebSocketServer.hpp" + +// 全局变量用于信号处理 +std::atomic g_running{true}; + +// 从文件读取API密钥 +std::string read_api_key_from_file(const std::string& filepath) { + std::ifstream file(filepath); + if (!file.is_open()) { + throw std::runtime_error("无法打开密钥文件: " + filepath); + } + + std::string api_key; + std::getline(file, api_key); + + // 去除可能的空白字符 + api_key.erase(0, api_key.find_first_not_of(" \t\n\r\f\v")); + api_key.erase(api_key.find_last_not_of(" \t\n\r\f\v") + 1); + + if (api_key.empty()) { + throw std::runtime_error("密钥文件为空: " + filepath); + } + + return api_key; +} + +int main(int argc, char* argv[]) { + // 解析命令行参数 + std::string api_key; + unsigned short port = 8080; + std::string key_file_path = std::string(getenv("HOME")) + "/.cert/deepseek.key"; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--key-file" && i + 1 < argc) { + key_file_path = argv[++i]; + } else if (arg == "--port" && i + 1 < argc) { + port = static_cast(std::stoi(argv[++i])); + } else if (arg == "--help") { + std::cout << "用法: " << argv[0] << " [--key-file <密钥文件路径>] [--port <端口号>]" << std::endl; + std::cout << "默认密钥文件路径: ~/.cert/deepseek.key" << std::endl; + return 0; + } + } + + try { + // 从文件读取API密钥 + std::cout << "正在从文件加载API密钥: " << key_file_path << std::endl; + api_key = read_api_key_from_file(key_file_path); + std::cout << "API密钥加载成功" << std::endl; + } catch (const std::exception& e) { + std::cerr << "错误: " << e.what() << std::endl; + std::cerr << "请确保密钥文件存在且包含有效的API密钥" << std::endl; + std::cerr << "默认路径: ~/.cert/deepseek.key" << std::endl; + std::cerr << "或使用 --key-file 参数指定其他路径" << std::endl; + return 1; + } + + // 创建并启动服务器 + pmc::llm::LLMWebSocketServer server(api_key, port); + + // 设置信号处理 + signal(SIGINT, [](int) { + g_running = false; + }); + + try { + server.start(); + + // 主循环,等待信号 + while (g_running) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // 停止服务器 + server.stop(); + + } catch (const std::exception& e) { + std::cerr << "服务器错误: " << e.what() << std::endl; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/lci/src/mainAgentFrontend.cpp b/lci/src/mainAgentFrontend.cpp new file mode 100644 index 0000000..2cfcb0d --- /dev/null +++ b/lci/src/mainAgentFrontend.cpp @@ -0,0 +1,582 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace http = beast::http; +namespace websocket = beast::websocket; +namespace net = boost::asio; +namespace ssl = net::ssl; +using tcp = boost::asio::ip::tcp; + +// 获取 MIME 类型 +std::string get_mime_type(const std::string& path) { + if (path.ends_with(".html") || path.ends_with(".htm")) return "text/html"; + if (path.ends_with(".css")) return "text/css"; + if (path.ends_with(".js")) return "application/javascript"; + if (path.ends_with(".json")) return "application/json"; + if (path.ends_with(".png")) return "image/png"; + if (path.ends_with(".jpg") || path.ends_with(".jpeg")) return "image/jpeg"; + if (path.ends_with(".gif")) return "image/gif"; + if (path.ends_with(".svg")) return "image/svg+xml"; + if (path.ends_with(".ico")) return "image/x-icon"; + return "text/plain"; +} + +// 读取文件内容 +std::string read_file(const std::string& path) { + try { + std::ifstream file(path, std::ios::binary); + if (!file) return ""; + + std::string content; + file.seekg(0, std::ios::end); + content.resize(file.tellg()); + file.seekg(0, std::ios::beg); + file.read(&content[0], content.size()); + return content; + } catch (...) { + return ""; + } +} + +// WebSocket 客户端,用于连接LLM后端 +class WebSocketClient : public std::enable_shared_from_this { +public: + WebSocketClient(net::io_context& ioc, const std::string& host, const std::string& port) + : resolver_(net::make_strand(ioc)) + , ws_(net::make_strand(ioc)) + , host_(host) + , port_(port) + , connected_(false) {} + + void connect() { + resolver_.async_resolve(host_, port_, + beast::bind_front_handler(&WebSocketClient::on_resolve, shared_from_this())); + } + + void send(const std::string& message) { + if (!connected_) { + std::cerr << "Cannot send: not connected to backend" << std::endl; + return; + } + + // 将消息放入队列 + { + std::lock_guard lock(queue_mutex_); + send_queue_.push(message); + } + + // 如果当前没有在发送,开始发送 + if (!sending_) { + send_next(); + } + } + + void set_message_handler(std::function handler) { + message_handler_ = handler; + } + + bool is_connected() const { return connected_; } + +private: + + void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) { + std::cerr << "Resolve error: " << ec.message() << std::endl; + return; + } + + beast::get_lowest_layer(ws_).async_connect(results, + beast::bind_front_handler(&WebSocketClient::on_connect, shared_from_this())); + } + + void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) { + if (ec) { + std::cerr << "Connect error: " << ec.message() << std::endl; + return; + } + + ws_.async_handshake(host_, "/", + beast::bind_front_handler(&WebSocketClient::on_handshake, shared_from_this())); + } + + void on_handshake(beast::error_code ec) { + if (ec) { + std::cerr << "Handshake error: " << ec.message() << std::endl; + return; + } + + std::cout << "Connected to LLM backend at " << host_ << ":" << port_ << std::endl; + connected_ = true; + + // 开始接收消息 + do_read(); + + // 发送欢迎消息 + send_welcome(); + } + + void send_welcome() { + std::string welcome_msg = R"({"type": "welcome", "message": "Connected to PMC LLM Backend"})"; + send(welcome_msg); + } + + void do_read() { + ws_.async_read(buffer_, + beast::bind_front_handler(&WebSocketClient::on_read, shared_from_this())); + } + + void on_read(beast::error_code ec, std::size_t bytes_transferred) { + if (ec == websocket::error::closed) { + std::cout << "WebSocket connection closed" << std::endl; + connected_ = false; + return; + } + + if (ec) { + std::cerr << "Read error: " << ec.message() << std::endl; + connected_ = false; + return; + } + + std::string message = beast::buffers_to_string(buffer_.data()); + buffer_.consume(buffer_.size()); + + // 处理接收到的消息 + if (message_handler_) { + message_handler_(message); + } + + // 继续读取 + do_read(); + } + + void send_next() { + std::lock_guard lock(queue_mutex_); + + if (send_queue_.empty()) { + sending_ = false; + return; + } + + sending_ = true; + std::string message = send_queue_.front(); + send_queue_.pop(); + + ws_.async_write(net::buffer(message), + beast::bind_front_handler(&WebSocketClient::on_write, shared_from_this())); + } + + void on_write(beast::error_code ec, std::size_t bytes_transferred) { + if (ec) { + std::cerr << "Write error: " << ec.message() << std::endl; + connected_ = false; + return; + } + + // 发送下一个消息 + send_next(); + } + + tcp::resolver resolver_; + websocket::stream ws_; + beast::flat_buffer buffer_; + std::string host_; + std::string port_; + std::atomic connected_; + + std::queue send_queue_; + std::mutex queue_mutex_; + std::atomic sending_{false}; + + std::function message_handler_; +}; + +// HTTP 请求处理器 +class HttpSession : public std::enable_shared_from_this { +public: + HttpSession(tcp::socket socket, const std::string& static_dir, + std::shared_ptr ws_client) + : socket_(std::move(socket)), static_dir_(static_dir), ws_client_(ws_client) {} + + void start() { + read_request(); + } + +private: + void read_request() { + auto self = shared_from_this(); + + http::async_read(socket_, buffer_, request_, + [self](beast::error_code ec, std::size_t bytes_transferred) { + if (!ec) { + self->process_request(); + } + }); + } + + void process_request() { + response_.version(request_.version()); + response_.keep_alive(false); + + // 检查是否为 WebSocket 升级请求 + if (websocket::is_upgrade(request_)) { + handle_websocket_upgrade(); + return; + } + + // 只处理 GET 请求 + if (request_.method() != http::verb::get) { + response_.result(http::status::bad_request); + response_.set(http::field::content_type, "text/plain"); + response_.body() = "Invalid method\n"; + write_response(); + return; + } + + // 获取请求路径 + std::string target = request_.target(); + if (target.empty() || target == "/") { + target = "/llm_chat.html"; // 默认加载聊天界面 + } + + // 防止路径遍历攻击 + if (target.find("..") != std::string::npos) { + response_.result(http::status::forbidden); + response_.set(http::field::content_type, "text/plain"); + response_.body() = "Forbidden\n"; + write_response(); + return; + } + + // 构建文件路径 + std::string file_path = static_dir_ + target; + + // 读取文件 + std::string content = read_file(file_path); + + if (content.empty()) { + // 文件不存在,返回 404 + if (target == "/llm_chat.html") { + // 如果聊天界面不存在,返回默认页面 + response_.result(http::status::ok); + response_.set(http::field::content_type, "text/html"); + response_.body() = "

PMC LLM Chat Interface

Chat interface not found. Please check static directory.

"; + } else { + response_.result(http::status::not_found); + response_.set(http::field::content_type, "text/plain"); + response_.body() = "File not found: " + target + "\n"; + } + } else { + // 文件存在,返回内容 + response_.result(http::status::ok); + response_.set(http::field::content_type, get_mime_type(file_path)); + response_.body() = content; + } + + write_response(); + } + + void handle_websocket_upgrade() { + try { + // 创建 WebSocket 会话 + auto ws = std::make_shared>(std::move(socket_)); + + // 接受 WebSocket 握手 + ws->async_accept(request_, + [self = shared_from_this(), ws](beast::error_code ec) { + if (!ec) { + self->handle_websocket_session(ws); + } + }); + } catch (const std::exception& e) { + std::cerr << "WebSocket upgrade error: " << e.what() << std::endl; + } + } + + void handle_websocket_session(std::shared_ptr> ws) { + std::cout << "New WebSocket connection established" << std::endl; + + // 发送欢迎消息 + std::string welcome_msg = R"({"type": "welcome", "message": "Welcome to PMC LLM Chat Interface!"})"; + ws->write(net::buffer(welcome_msg)); + + // 开始读取消息 + do_websocket_read(ws); + } + + void do_websocket_read(std::shared_ptr> ws) { + auto buffer = std::make_shared(); + + ws->async_read(*buffer, + [self = shared_from_this(), ws, buffer](beast::error_code ec, std::size_t bytes_transferred) { + if (ec == websocket::error::closed) { + std::cout << "WebSocket connection closed by client" << std::endl; + return; + } + + if (ec) { + std::cerr << "WebSocket read error: " << ec.message() << std::endl; + return; + } + + // 处理接收到的消息 + std::string message = beast::buffers_to_string(buffer->data()); + self->handle_websocket_message(ws, message); + + // 继续读取 + self->do_websocket_read(ws); + }); + } + + void handle_websocket_message(std::shared_ptr> ws, + const std::string& message) { + try { + std::cout << "Received WebSocket message: " << message << std::endl; + + // 解析 JSON 消息 + // 这里简化处理,实际应该使用 JSON 库 + if (message.find("\"type\":\"hello\"") != std::string::npos || + message.find("hello") != std::string::npos) { + // 发送欢迎消息 + std::string response = R"({"type": "welcome", "message": "Hello! I'm PMC LLM Assistant. How can I help you today?"})"; + ws->write(net::buffer(response)); + } else if (message.find("\"type\":\"message\"") != std::string::npos || + message.find("\"type\":\"chat\"") != std::string::npos) { + // 聊天消息,转发到 LLM 后端 + if (ws_client_ && ws_client_->is_connected()) { + // 发送思考指示 + std::string thinking_msg = R"({"type": "thinking"})"; + ws->write(net::buffer(thinking_msg)); + + // 转发到 LLM 后端 + ws_client_->send(message); + + // 设置消息处理器,将后端响应转发回客户端 + ws_client_->set_message_handler([this, ws](const std::string& backend_response) { + // 将后端响应转发给 WebSocket 客户端 + std::string response = R"({"type": "response", "content": ")" + + escape_json(backend_response) + "\"}"; + ws->write(net::buffer(response)); + }); + } else { + // LLM 后端未连接,返回模拟响应 + std::string response = R"({"type": "response", "content": "LLM backend is not connected. This is a simulated response."})"; + ws->write(net::buffer(response)); + } + } else if (message.find("\"type\":\"change_model\"") != std::string::npos) { + // 处理模型切换请求 + std::string response = R"({"type": "system", "message": "Model change request received"})"; + ws->write(net::buffer(response)); + } else { + // 默认处理:直接回复 + std::string response = R"({"type": "response", "content": "I received your message: )" + + escape_json(message) + "\"}"; + ws->write(net::buffer(response)); + } + } catch (const std::exception& e) { + std::cerr << "Error handling WebSocket message: " << e.what() << std::endl; + std::string error_msg = R"({"type": "error", "message": "Error processing message"})"; + ws->write(net::buffer(error_msg)); + } + } + + std::string escape_json(const std::string& input) { + std::string output; + output.reserve(input.length()); + + for (char c : input) { + switch (c) { + case '"': output += "\\\""; break; + case '\\': output += "\\\\"; break; + case '\b': output += "\\b"; break; + case '\f': output += "\\f"; break; + case '\n': output += "\\n"; break; + case '\r': output += "\\r"; break; + case '\t': output += "\\t"; break; + default: output += c; break; + } + } + + return output; + } + + void write_response() { + auto self = shared_from_this(); + + response_.content_length(response_.body().size()); + + http::async_write(socket_, response_, + [self](beast::error_code ec, std::size_t bytes_transferred) { + // 连接关闭后自动清理 + }); + } + + tcp::socket socket_; + beast::flat_buffer buffer_; + http::request request_; + http::response response_; + std::string static_dir_; + std::shared_ptr ws_client_; +}; + +// HTTP 服务器 +class HttpServer { +public: + HttpServer(net::io_context& ioc, tcp::endpoint endpoint, + const std::string& static_dir, const std::string& backend_host, + const std::string& backend_port) + : acceptor_(ioc) + , static_dir_(static_dir) + , backend_host_(backend_host) + , backend_port_(backend_port) { + + beast::error_code ec; + + // 打开 acceptor + acceptor_.open(endpoint.protocol(), ec); + if (ec) { + std::cerr << "Open error: " << ec.message() << std::endl; + return; + } + + // 设置地址重用 + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if (ec) { + std::cerr << "Set option error: " << ec.message() << std::endl; + return; + } + + // 绑定到地址 + acceptor_.bind(endpoint, ec); + if (ec) { + std::cerr << "Bind error: " << ec.message() << std::endl; + return; + } + + // 开始监听 + acceptor_.listen(net::socket_base::max_listen_connections, ec); + if (ec) { + std::cerr << "Listen error: " << ec.message() << std::endl; + return; + } + + std::cout << "HTTP server listening on " << endpoint.address().to_string() + << ":" << endpoint.port() << std::endl; + + // 创建 WebSocket 客户端连接到 LLM 后端 + ws_client_ = std::make_shared(ioc, backend_host_, backend_port_); + + // 尝试连接到后端 + std::cout << "Connecting to LLM backend at " << backend_host_ << ":" << backend_port_ << std::endl; + ws_client_->connect(); + + // 开始接受连接 + do_accept(); + } + +private: + void do_accept() { + acceptor_.async_accept( + [this](beast::error_code ec, tcp::socket socket) { + if (!ec) { + std::make_shared(std::move(socket), static_dir_, ws_client_)->start(); + } + + // 继续接受新连接 + do_accept(); + }); + } + + tcp::acceptor acceptor_; + std::string static_dir_; + std::string backend_host_; + std::string backend_port_; + std::shared_ptr ws_client_; +}; + +int main(int argc, char* argv[]) { + try { + // 默认配置 + std::string host = "0.0.0.0"; + unsigned short port = 8080; + std::string static_dir = "./static"; + std::string backend_host = "localhost"; + std::string backend_port = "8081"; + + // 解析命令行参数 + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--host" && i + 1 < argc) { + host = argv[++i]; + } else if (arg == "--port" && i + 1 < argc) { + port = static_cast(std::stoi(argv[++i])); + } else if (arg == "--static" && i + 1 < argc) { + static_dir = argv[++i]; + } else if (arg == "--backend-host" && i + 1 < argc) { + backend_host = argv[++i]; + } else if (arg == "--backend-port" && i + 1 < argc) { + backend_port = argv[++i]; + } else if (arg == "--help") { + std::cout << "Usage: " << argv[0] << " [options]\n" + << "Options:\n" + << " --host
HTTP server host (default: 0.0.0.0)\n" + << " --port HTTP server port (default: 8080)\n" + << " --static Static files directory (default: ./static)\n" + << " --backend-host LLM backend host (default: localhost)\n" + << " --backend-port LLM backend port (default: 8081)\n" + << " --help Show this help message\n"; + return 0; + } + } + + // 检查静态目录 + if (!std::filesystem::exists(static_dir)) { + std::cout << "Static directory '" << static_dir << "' does not exist. Creating it..." << std::endl; + std::filesystem::create_directories(static_dir); + + // 创建默认的 llm_chat.html 文件 + std::string chat_html_path = static_dir + "/llm_chat.html"; + if (!std::filesystem::exists(chat_html_path)) { + std::cout << "Creating default chat interface..." << std::endl; + // 这里可以添加默认的 HTML 内容 + } + } + + std::cout << "PMC LLM Frontend Server\n"; + std::cout << "=======================\n"; + std::cout << "HTTP Server: " << host << ":" << port << "\n"; + std::cout << "Static Directory: " << static_dir << "\n"; + std::cout << "LLM Backend: " << backend_host << ":" << backend_port << "\n"; + std::cout << "==============================\n\n"; + + // 创建 I/O 上下文 + net::io_context ioc; + + // 创建 HTTP 服务器 + tcp::endpoint endpoint(net::ip::make_address(host), port); + HttpServer server(ioc, endpoint, static_dir, backend_host, backend_port); + + // 运行 I/O 上下文 + ioc.run(); + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/lci/src/mainCamera.cpp b/lci/src/mainCamera.cpp new file mode 100644 index 0000000..54d2200 --- /dev/null +++ b/lci/src/mainCamera.cpp @@ -0,0 +1,16 @@ +#include +#include "hd/Camera.h" + +int main() { + + Camera_init(640, 480); + + std::string imgpath = std::to_string(time(NULL)) + ".jpeg"; + std::string txtpath =std::to_string(time(NULL)) + ".txt"; + + bool flag; + auto img = get_frame_from_camera(flag); + img.save(imgpath.c_str()); + + Camera_close(); +} \ No newline at end of file diff --git a/lci/src/mainCameraClient.py b/lci/src/mainCameraClient.py new file mode 100644 index 0000000..63e45fb --- /dev/null +++ b/lci/src/mainCameraClient.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +相机客户端Python实现 +功能:连接服务器接收YUYV格式视频流,转换为RGB并显示 +""" + +import socket +import cv2 +import numpy as np +import sys + + +def main(): + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ") + return 1 + + server_ip = sys.argv[1] + port = int(sys.argv[2]) + + # 创建TCP套接字 + sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + try: + # 连接到服务器 + sockfd.connect((server_ip, port)) + print(f"Connected to server: {server_ip}:{port}") + + # 图像参数 + WIDTH = 640 + HEIGHT = 480 + FRAME_BUFFER_SIZE = WIDTH * HEIGHT * 2 # YUYV格式:640x480,每个像素2字节 + + # OpenCV窗口 + cv2.namedWindow("Camera Stream", cv2.WINDOW_AUTOSIZE) + + frame_count = 0 + while True: + # 接收帧数据 + frame_buffer = b'' + while len(frame_buffer) < FRAME_BUFFER_SIZE: + data = sockfd.recv(FRAME_BUFFER_SIZE - len(frame_buffer)) + if not data: + print("Server closed connection") + return 0 + frame_buffer += data + + print(f"Frame {frame_count + 1} received: {len(frame_buffer)} bytes") + frame_count += 1 + + # 使用OpenCV处理YUYV帧 + yuyv_frame = np.frombuffer(frame_buffer, dtype=np.uint8).reshape((HEIGHT, WIDTH, 2)) + rgb_frame = cv2.cvtColor(yuyv_frame, cv2.COLOR_YUV2BGR_YUYV) + + # 显示帧 + cv2.imshow("Camera Stream", rgb_frame) + + # 等待1ms,允许窗口刷新,同时检查是否按下ESC键退出 + if cv2.waitKey(1) == 27: + print("ESC key pressed, exiting...") + break + + except Exception as e: + print(f"Error: {e}") + return 1 + finally: + # 清理资源 + sockfd.close() + cv2.destroyAllWindows() + print("Client exiting") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/lci/src/mainCompressedCameraClient.py b/lci/src/mainCompressedCameraClient.py new file mode 100644 index 0000000..b4dc28c --- /dev/null +++ b/lci/src/mainCompressedCameraClient.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +压缩相机客户端Python实现 +功能:连接服务器接收JPEG压缩视频流,解码并显示 +""" + +import socket +import cv2 +import numpy as np +import sys +import time +from PIL import Image +import io + + +def main(): + print("Starting Compressed Camera Client...") + + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ") + return 1 + + server_ip = sys.argv[1] + port = int(sys.argv[2]) + + print(f"Configuration: Server IP={server_ip}, Port={port}") + + # 创建TCP套接字 + print("Creating TCP socket...") + sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # 设置套接字超时 + sockfd.settimeout(5.0) + print("TCP socket created successfully") + + try: + # 连接到服务器 + print(f"Connecting to server {server_ip}:{port}...") + sockfd.connect((server_ip, port)) + print(f"Connected to server: {server_ip}:{port}") + + # 关闭超时,改为非阻塞模式 + sockfd.setblocking(True) + print("Socket set to blocking mode") + + # OpenCV窗口 + print("Creating OpenCV window...") + # 尝试使用不同的窗口参数 + cv2.namedWindow("Compressed Camera Stream", cv2.WINDOW_GUI_NORMAL) + # 设置固定的窗口大小 + cv2.resizeWindow("Compressed Camera Stream", 640, 480) + # 确保窗口大小是固定的 + cv2.setWindowProperty("Compressed Camera Stream", cv2.WND_PROP_ASPECT_RATIO, cv2.WINDOW_KEEPRATIO) + cv2.setWindowProperty("Compressed Camera Stream", cv2.WND_PROP_AUTOSIZE, cv2.WINDOW_AUTOSIZE) + + # 显示初始信息 + info_frame = np.zeros((480, 640, 3), dtype=np.uint8) + cv2.putText(info_frame, "Connecting to server...", (50, 240), + cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) + cv2.imshow("Compressed Camera Stream", info_frame) + cv2.waitKey(100) + + frame_count = 0 + buffer = b'' # 用于累积数据的缓冲区 + total_bytes_received = 0 + jpeg_found_count = 0 + + # 上次收到数据的时间 + last_receive_time = time.time() + + while True: + try: + # 接收数据 + data = sockfd.recv(8192) + if not data: + print("Server closed connection") + return 0 + + last_receive_time = time.time() + total_bytes_received += len(data) + print(f"Received {len(data)} bytes, total: {total_bytes_received} bytes") + + # 将新数据添加到缓冲区 + buffer += data + + # 仅保留最近的500KB数据,防止内存溢出 + if len(buffer) > 500 * 1024: + print(f"Buffer size exceeded 500KB, trimming to 500KB") + buffer = buffer[-500 * 1024:] + + # 检查缓冲区中是否有JPEG标记 + has_jpeg = b'\xff\xd8' in buffer + print(f"Buffer contains JPEG SOI: {has_jpeg}, buffer size: {len(buffer)}") + + # JPEG文件格式标记 + JPEG_START = b'\xff\xd8' # SOI (Start of Image) + JPEG_END = b'\xff\xd9' # EOI (End of Image) + + # 查找所有完整的JPEG帧 + while True: + # 查找JPEG开始标记 + start_idx = buffer.find(JPEG_START) + if start_idx == -1: + # 没有找到开始标记 + print(f"No JPEG SOI found, buffer size: {len(buffer)}") + # 清空大部分缓冲区,只保留末尾可能包含部分JPEG头的数据 + if len(buffer) > 1024: + buffer = buffer[-1024:] + print(f"Trimmed buffer to {len(buffer)} bytes") + break + + # 查找当前开始标记后的结束标记 + end_idx = buffer.find(JPEG_END, start_idx + 2) + if end_idx == -1: + # 没有找到完整的结束标记 + print(f"Found JPEG SOI but no EOI, buffer size from SOI: {len(buffer) - start_idx}") + # 保留从开始标记到缓冲区末尾的数据 + buffer = buffer[start_idx:] + break + + # 提取完整的JPEG帧(包括结束标记) + jpeg_frame = buffer[start_idx:end_idx + 2] + jpeg_found_count += 1 + print(f"Extracted JPEG frame {jpeg_found_count}, size: {len(jpeg_frame)} bytes") + + # 从缓冲区中移除已处理的JPEG帧 + buffer = buffer[end_idx + 2:] + print(f"Buffer remaining after extraction: {len(buffer)} bytes") + + # 只处理合理大小的JPEG帧(1KB到500KB) + if 1024 <= len(jpeg_frame) <= 500 * 1024: + try: + # 使用PIL库来解码和显示图像 + image_stream = io.BytesIO(jpeg_frame) + pil_image = Image.open(image_stream) + + if pil_image is not None: + # 解码成功,显示帧 + frame_count += 1 + print(f"Frame {frame_count} decoded successfully, size: {pil_image.width}x{pil_image.height}") + + # 转换为OpenCV格式 + opencv_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) + + # 检查图像是否有重复的模式 + # 从截图来看,图像被分割成了3x3的网格 + # 这可能是因为图像高度是正常的3倍 + if opencv_image.shape[0] == 1440 and opencv_image.shape[1] == 640: + # 高度是1440,是正常的3倍 + # 只显示图像的前480行 + fixed_image = opencv_image[:480, :, :] + cv2.imshow("Compressed Camera Stream", fixed_image) + print(f"Fixed frame by cropping to 640x480") + elif opencv_image.shape[0] == 640 and opencv_image.shape[1] == 1920: + # 宽度是1920,是正常的3倍 + # 只显示图像的前640列 + fixed_image = opencv_image[:, :640, :] + cv2.imshow("Compressed Camera Stream", fixed_image) + print(f"Fixed frame by cropping to 640x480") + else: + # 直接显示原始大小的图像 + cv2.imshow("Compressed Camera Stream", opencv_image) + + # 只在显示新帧后调用一次waitKey,确保图像刷新 + key = cv2.waitKey(1) + if key == 27: + print("ESC key pressed, exiting...") + return 1 + else: + print(f"Failed to decode JPEG frame {jpeg_found_count}") + except Exception as e: + print(f"Error decoding JPEG frame {jpeg_found_count}: {e}") + # 尝试使用OpenCV的方式解码 + try: + np_data = np.frombuffer(jpeg_frame, dtype=np.uint8) + frame = cv2.imdecode(np_data, cv2.IMREAD_COLOR) + if frame is not None: + frame_count += 1 + print(f"Frame {frame_count} decoded with OpenCV, size: {frame.shape[1]}x{frame.shape[0]}") + # 检查是否是3x3网格 + if frame.shape[0] > 480 and frame.shape[1] == 640: + # 只显示前480行 + cv2.imshow("Compressed Camera Stream", frame[:480, :, :]) + else: + cv2.imshow("Compressed Camera Stream", frame) + key = cv2.waitKey(1) + if key == 27: + print("ESC key pressed, exiting...") + return 1 + except Exception as e2: + print(f"Failed with OpenCV too: {e2}") + else: + print(f"Invalid JPEG frame size: {len(jpeg_frame)} bytes, skipped") + + except socket.timeout: + print("Socket timeout, no data received") + except Exception as e: + print(f"Error in receive loop: {e}") + + # 检查连接是否超时 + if time.time() - last_receive_time > 10: + print("Connection timeout: No data received for 10 seconds") + return 1 + + # 检查是否按下ESC键退出 + key = cv2.waitKey(1) + if key == 27: + print("ESC key pressed, exiting...") + break + + except socket.timeout as e: + print(f"Connection timeout: {e}") + return 1 + except Exception as e: + print(f"Error: {e}") + return 1 + finally: + # 清理资源 + print("Cleaning up resources...") + sockfd.close() + cv2.destroyAllWindows() + print(f"Client exiting. Total bytes received: {total_bytes_received}, JPEG frames found: {jpeg_found_count}, Successful frames: {frame_count}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/lci/src/mainDashunFront.cpp b/lci/src/mainDashunFront.cpp new file mode 100644 index 0000000..8fcae77 --- /dev/null +++ b/lci/src/mainDashunFront.cpp @@ -0,0 +1,287 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +using tcp = boost::asio::ip::tcp; +namespace fs = std::filesystem; + +// 获取MIME类型 +std::string get_mime_type(const std::string& path) { + if (path.ends_with(".html") || path.ends_with(".htm")) return "text/html"; + if (path.ends_with(".css")) return "text/css"; + if (path.ends_with(".js")) return "application/javascript"; + if (path.ends_with(".json")) return "application/json"; + if (path.ends_with(".png")) return "image/png"; + if (path.ends_with(".jpg") || path.ends_with(".jpeg")) return "image/jpeg"; + if (path.ends_with(".gif")) return "image/gif"; + if (path.ends_with(".svg")) return "image/svg+xml"; + if (path.ends_with(".txt")) return "text/plain"; + if (path.ends_with(".ico")) return "image/x-icon"; + return "application/octet-stream"; +} + +// 读取文件内容 +std::string read_file(const std::string& path) { + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) { + std::cerr << "无法打开文件: " << path << std::endl; + return ""; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); +} + +// 处理HTTP请求 +class http_connection : public std::enable_shared_from_this { +public: + http_connection(tcp::socket socket, const std::string& static_dir) + : socket_(std::move(socket)), static_dir_(static_dir) {} + + void start() { + read_request(); + } + +private: + void read_request() { + auto self = shared_from_this(); + + http::async_read(socket_, buffer_, request_, + [self](beast::error_code ec, std::size_t bytes_transferred) { + if (!ec) { + self->process_request(); + } + }); + } + + void process_request() { + response_.version(request_.version()); + response_.keep_alive(false); + + std::string target = std::string(request_.target()); + + // 防止路径遍历攻击 + if (target.find("..") != std::string::npos) { + response_.result(http::status::forbidden); + response_.set(http::field::content_type, "text/plain"); + response_.body() = "禁止访问"; + send_response(); + return; + } + + // 处理根路径 + if (target == "/" || target == "/index.html") { + serve_file("dashun_index.html"); + return; + } + + // 处理健康检查 + if (target == "/health") { + response_.result(http::status::ok); + response_.set(http::field::content_type, "text/plain"); + response_.body() = "大顺脚本前端服务运行正常"; + send_response(); + return; + } + + // 处理API请求 + if (target.starts_with("/api/")) { + handle_api_request(target); + return; + } + + // 处理静态文件 + serve_file(target.substr(1)); // 移除开头的'/' + } + + void handle_api_request(const std::string& target) { + response_.result(http::status::ok); + response_.set(http::field::content_type, "application/json"); + + if (target == "/api/status") { + response_.body() = R"({ + "status": "running", + "version": "1.0.0", + "uptime": "0", + "requests_served": 0 + })"; + } else if (target == "/api/info") { + response_.body() = R"({ + "name": "大顺脚本前端服务", + "description": "为pmc项目提供Web界面", + "author": "大顺", + "repository": "https://github.com/your-repo/pmc" + })"; + } else { + response_.result(http::status::not_found); + response_.set(http::field::content_type, "application/json"); + response_.body() = R"({"error": "API endpoint not found"})"; + } + + send_response(); + } + + void serve_file(const std::string& filename) { + std::string filepath = static_dir_ + "/" + filename; + + // 安全检查:确保文件在静态目录内 + fs::path requested_path = fs::absolute(filepath); + fs::path static_path = fs::absolute(static_dir_); + + if (!fs::equivalent(requested_path.parent_path(), static_path) && + !requested_path.string().starts_with(static_path.string() + "/")) { + response_.result(http::status::forbidden); + response_.set(http::field::content_type, "text/plain"); + response_.body() = "禁止访问"; + send_response(); + return; + } + + if (!fs::exists(filepath)) { + response_.result(http::status::not_found); + response_.set(http::field::content_type, "text/plain"); + response_.body() = "文件不存在: " + filename; + send_response(); + return; + } + + std::string content = read_file(filepath); + if (content.empty()) { + response_.result(http::status::internal_server_error); + response_.set(http::field::content_type, "text/plain"); + response_.body() = "读取文件失败"; + send_response(); + return; + } + + response_.result(http::status::ok); + response_.set(http::field::content_type, get_mime_type(filename)); + response_.body() = content; + send_response(); + } + + void send_response() { + auto self = shared_from_this(); + + response_.content_length(response_.body().size()); + + http::async_write(socket_, response_, + [self](beast::error_code ec, std::size_t) { + self->socket_.shutdown(tcp::socket::shutdown_send, ec); + }); + } + + tcp::socket socket_; + beast::flat_buffer buffer_{8192}; + http::request request_; + http::response response_; + std::string static_dir_; +}; + +// HTTP服务器 +class http_server { +public: + http_server(net::io_context& ioc, tcp::endpoint endpoint, const std::string& static_dir) + : acceptor_(ioc), static_dir_(static_dir) { + beast::error_code ec; + + // 打开acceptor + acceptor_.open(endpoint.protocol(), ec); + if (ec) { + throw beast::system_error{ec}; + } + + // 设置地址重用 + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if (ec) { + throw beast::system_error{ec}; + } + + // 绑定到端点 + acceptor_.bind(endpoint, ec); + if (ec) { + throw beast::system_error{ec}; + } + + // 开始监听 + acceptor_.listen(net::socket_base::max_listen_connections, ec); + if (ec) { + throw beast::system_error{ec}; + } + } + + void start_accept() { + acceptor_.async_accept( + [this](beast::error_code ec, tcp::socket socket) { + if (!ec) { + std::make_shared(std::move(socket), static_dir_)->start(); + } + start_accept(); + }); + } + +private: + tcp::acceptor acceptor_; + std::string static_dir_; +}; + +int main() { + try { + // 静态文件目录 + std::string static_dir = "static"; + + // 确保静态目录存在 + if (!fs::exists(static_dir)) { + std::cerr << "警告: 静态目录 '" << static_dir << "' 不存在" << std::endl; + if (!fs::create_directory(static_dir)) { + std::cerr << "错误: 无法创建静态目录" << std::endl; + return 1; + } + } + + // 检查主页文件是否存在 + std::string homepage = static_dir + "/dashun_index.html"; + if (!fs::exists(homepage)) { + std::cerr << "警告: 主页文件 '" << homepage << "' 不存在" << std::endl; + std::cerr << "请确保 dashun_index.html 文件在 static/ 目录中" << std::endl; + } + + // 创建IO上下文 + net::io_context ioc{1}; + + // 创建服务器端点(监听所有地址的8080端口) + tcp::endpoint endpoint{tcp::v4(), 8080}; + + // 创建HTTP服务器 + http_server server{ioc, endpoint, static_dir}; + server.start_accept(); + + // 显示启动信息 + std::cout << "==========================================" << std::endl; + std::cout << "大顺脚本前端服务已启动" << std::endl; + std::cout << "监听地址: http://0.0.0.0:8080" << std::endl; + std::cout << "静态文件目录: " << static_dir << std::endl; + std::cout << "按 Ctrl+C 停止服务" << std::endl; + std::cout << "==========================================" << std::endl; + + ioc.run(); + + } catch (const std::exception& e) { + std::cerr << "错误: " << e.what() << std::endl; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/lci/src/mainGui.cpp b/lci/src/mainGui.cpp new file mode 100644 index 0000000..abebedd --- /dev/null +++ b/lci/src/mainGui.cpp @@ -0,0 +1,410 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "llm/LLMClient.hpp" + +namespace pmc::gui::example { + +// 带缓冲区的Markdown转换器,累积整个响应并处理Markdown +class BufferedMarkdownConverter { +private: + std::string buffer; // 累积原始Markdown文本 + std::string last_processed; // 上次处理的Pango标记 + +public: + BufferedMarkdownConverter() {} + + // 重置状态 + void reset() { + buffer.clear(); + last_processed.clear(); + } + + // 添加新的Markdown块到缓冲区 + void append_chunk(const std::string& chunk) { + buffer += chunk; + } + + // 获取当前缓冲区的内容 + std::string get_buffer() const { + return buffer; + } + + // 处理整个缓冲区并返回Pango标记 + std::string process_buffer() { + std::string result = buffer; + + // 1. 首先转义HTML特殊字符 + result = std::regex_replace(result, std::regex("&"), "&"); + result = std::regex_replace(result, std::regex("<"), "<"); + result = std::regex_replace(result, std::regex(">"), ">"); + + // 2. 转换粗斜体:***text*** -> text + // 先处理粗斜体,确保正确嵌套 + std::regex bold_italic_regex("\\*\\*\\*(.*?)\\*\\*\\*"); + result = std::regex_replace(result, bold_italic_regex, "$1"); + + // 3. 转换粗体:**text** -> text(但排除已经是粗斜体的部分) + std::regex bold_regex("\\*\\*(?!\\*)(.*?)\\*\\*"); + result = std::regex_replace(result, bold_regex, "$1"); + + // 4. 转换斜体:*text* -> text(但排除已经是粗体或粗斜体的部分) + std::regex italic_regex("\\*(?!\\*)(.*?)\\*"); + result = std::regex_replace(result, italic_regex, "$1"); + + // 5. 转换内联代码:`code` -> code (使用标签而不是) + std::regex code_regex("`(.*?)`"); + result = std::regex_replace(result, code_regex, "$1"); + + // 6. 转换标题:# 标题 -> 标题 + std::regex h1_regex("^#\\s+(.*?)$", std::regex::multiline); + result = std::regex_replace(result, h1_regex, "$1\n"); + + std::regex h2_regex("^##\\s+(.*?)$", std::regex::multiline); + result = std::regex_replace(result, h2_regex, "$1\n"); + + std::regex h3_regex("^###\\s+(.*?)$", std::regex::multiline); + result = std::regex_replace(result, h3_regex, "$1\n"); + + // 7. 转换无序列表:* item -> • item + std::regex ul_regex("^\\*\\s+(.*?)$", std::regex::multiline); + result = std::regex_replace(result, ul_regex, "• $1\n"); + + // 8. 转换有序列表:1. item -> 1. item + std::regex ol_regex("^\\d+\\.\\s+(.*?)$", std::regex::multiline); + // 保持原样,只是确保有换行 + result = std::regex_replace(result, ol_regex, "$&\n"); + + // 9. 转换引用:> text -> text + std::regex blockquote_regex("^>\\s+(.*?)$", std::regex::multiline); + result = std::regex_replace(result, blockquote_regex, "$1\n"); + + // 10. 转换链接:[text](url) -> text (Pango不支持标签,所以只显示文本) + std::regex link_regex("\\[(.*?)\\]\\((.*?)\\)"); + result = std::regex_replace(result, link_regex, "$1"); + + // 11. 转换图片:![alt](url) -> [图片: alt] (Pango不支持图片,所以显示替代文本) + std::regex image_regex("!\\[(.*?)\\]\\((.*?)\\)"); + result = std::regex_replace(result, image_regex, "[图片: $1]"); + + // 重要修复:Pango标记语言使用 \n 表示换行,而不是
+ // 不要转换换行符,保持原样 + + last_processed = result; + return result; + } + + // 获取上次处理的结果 + std::string get_last_processed() const { + return last_processed; + } +}; + +/** + * @brief 简单的聊天窗口类 + */ +class SimpleChatWindow { +private: + GtkTextMark* current_response_start_mark; // 标记当前响应的开始位置 + +public: + SimpleChatWindow() : current_response_start_mark(nullptr) { + // 创建主窗口 + window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL)); + gtk_window_set_title(window, "PMC Chat"); + gtk_window_set_default_size(window, 800, 600); + g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), nullptr); + + // 创建主垂直布局 + GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); + gtk_container_add(GTK_CONTAINER(window), vbox); + + // 创建滚动窗口和文本视图(用于显示聊天记录) + GtkWidget* scrolled_window = gtk_scrolled_window_new(nullptr, nullptr); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_box_pack_start(GTK_BOX(vbox), scrolled_window, TRUE, TRUE, 0); + + text_view = GTK_TEXT_VIEW(gtk_text_view_new()); + gtk_text_view_set_editable(text_view, FALSE); + gtk_text_view_set_cursor_visible(text_view, FALSE); + gtk_text_view_set_wrap_mode(text_view, GTK_WRAP_WORD_CHAR); + gtk_container_add(GTK_CONTAINER(scrolled_window), GTK_WIDGET(text_view)); + + // 设置文本视图的边距 + gtk_text_view_set_left_margin(text_view, 10); + gtk_text_view_set_right_margin(text_view, 10); + gtk_text_view_set_top_margin(text_view, 10); + gtk_text_view_set_bottom_margin(text_view, 10); + + // 创建输入区域的水平布局 + GtkWidget* hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); + + // 创建输入框 + entry = GTK_ENTRY(gtk_entry_new()); + gtk_entry_set_placeholder_text(entry, "输入消息..."); + gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(entry), TRUE, TRUE, 0); + + // 创建发送按钮 + send_button = GTK_BUTTON(gtk_button_new_with_label("发送")); + gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(send_button), FALSE, FALSE, 0); + + // 连接信号 + g_signal_connect(send_button, "clicked", G_CALLBACK(on_send_clicked), this); + g_signal_connect(entry, "activate", G_CALLBACK(on_entry_activate), this); + + // 初始化LLM客户端 + std::string api_key; + const char* env_key = std::getenv("DEEPSEEK_API_KEY"); + if (env_key && env_key[0] != '\0') { + api_key = env_key; + } else { + // 尝试从文件读取 + std::string key_file = std::string(std::getenv("HOME")) + "/.cert/deepseek.key"; + std::ifstream file(key_file); + if (file.is_open()) { + std::getline(file, api_key); + file.close(); + } else { + api_key = ""; // 使用空密钥,会在使用时出错 + } + } + + llm_client = std::make_unique(api_key); + markdown_converter = std::make_unique(); + } + + // 使用带缓冲区的Markdown转换器进行转换 + std::string markdown_to_pango(const std::string& markdown) { + // 将新块添加到缓冲区 + markdown_converter->append_chunk(markdown); + + // 处理整个缓冲区 + std::string result = markdown_converter->process_buffer(); + + return result; + } + + void show() { + gtk_widget_show_all(GTK_WIDGET(window)); + } + + void append_message(const std::string& message) { + GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view); + GtkTextIter end; + gtk_text_buffer_get_end_iter(buffer, &end); + + // 用户消息不需要Markdown转换,直接显示 + gtk_text_buffer_insert(buffer, &end, message.c_str(), -1); + gtk_text_buffer_insert(buffer, &end, "\n", -1); + + // 滚动到底部 + GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment( + GTK_SCROLLED_WINDOW(gtk_widget_get_parent(GTK_WIDGET(text_view)))); + gtk_adjustment_set_value(adj, gtk_adjustment_get_upper(adj)); + } + + void append_stream_chunk(const std::string& chunk) { + GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view); + + // 如果这是响应的第一个块,创建一个标记来跟踪开始位置 + if (current_response_start_mark == nullptr) { + GtkTextIter end; + gtk_text_buffer_get_end_iter(buffer, &end); + + // 添加助手前缀 + gtk_text_buffer_insert(buffer, &end, "Assistant: ", -1); + + // 在助手前缀后创建标记 + gtk_text_buffer_get_end_iter(buffer, &end); + current_response_start_mark = gtk_text_buffer_create_mark(buffer, "current_response_start", &end, TRUE); + } + + // 获取当前响应的开始位置 + GtkTextIter start_iter; + gtk_text_buffer_get_iter_at_mark(buffer, &start_iter, current_response_start_mark); + + // 获取缓冲区的结束位置 + GtkTextIter end_iter; + gtk_text_buffer_get_end_iter(buffer, &end_iter); + + // 删除当前响应区域(从标记位置到缓冲区末尾) + gtk_text_buffer_delete(buffer, &start_iter, &end_iter); + + // 在处理后的位置插入新的内容 + gtk_text_buffer_get_iter_at_mark(buffer, &start_iter, current_response_start_mark); + + // 关键修复:插入整个累积的缓冲区内容,而不仅仅是当前的chunk + // 首先,将新chunk添加到缓冲区 + markdown_converter->append_chunk(chunk); + + // 然后处理整个缓冲区 + std::string processed_buffer = markdown_converter->process_buffer(); + + // 插入处理后的整个缓冲区内容 + if (!processed_buffer.empty()) { + gtk_text_buffer_insert_markup(buffer, &start_iter, processed_buffer.c_str(), -1); + } + + // 滚动到底部 + GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment( + GTK_SCROLLED_WINDOW(gtk_widget_get_parent(GTK_WIDGET(text_view)))); + gtk_adjustment_set_value(adj, gtk_adjustment_get_upper(adj)); + + // 累积当前响应 + current_response += chunk; + } + + void finish_response() { + // 将完整的响应添加到聊天历史 + if (!current_response.empty()) { + chat_history.push_back({"assistant", current_response}); + current_response.clear(); + } + + // 重置Markdown转换器 + markdown_converter->reset(); + + // 清除当前响应标记 + if (current_response_start_mark != nullptr) { + GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view); + gtk_text_buffer_delete_mark(buffer, current_response_start_mark); + current_response_start_mark = nullptr; + } + + // 添加换行符 + GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view); + GtkTextIter end; + gtk_text_buffer_get_end_iter(buffer, &end); + gtk_text_buffer_insert(buffer, &end, "\n", -1); + } + + std::string get_input_text() { + const char* text = gtk_entry_get_text(entry); + return text ? std::string(text) : ""; + } + + void clear_input() { + gtk_entry_set_text(entry, ""); + } + + void send_to_llm(const std::string& user_message) { + // 将用户消息添加到聊天历史 + chat_history.push_back({"user", user_message}); + + // 创建流式回调函数 + auto stream_callback = [this](const std::string& chunk) { + // 关键修复:简化完成标记检查 + // 只检查明确的完成标记,避免误判 + if (chunk == "[[PMC_STREAM_COMPLETE]]" || chunk == "[流式传输完成]]") { + std::cout << "DEBUG: Detected completion marker" << std::endl; + // 流式传输完成,在主线程中调用finish_response + g_idle_add([](gpointer data) -> gboolean { + auto* self = static_cast(data); + self->finish_response(); + return FALSE; // 只执行一次 + }, this); + return; // 重要:直接返回,不显示这个字符串 + } + + // 使用g_idle_add在主线程中更新UI + g_idle_add([](gpointer data) -> gboolean { + auto* chunk_data = static_cast*>(data); + chunk_data->first->append_stream_chunk(chunk_data->second); + delete chunk_data; + return FALSE; // 只执行一次 + }, new std::pair(this, chunk)); + }; + + // 在新线程中发送流式请求(避免阻塞主线程) + std::thread([this, stream_callback]() { + try { + llm_client->stream_request(chat_history, stream_callback); + } catch (const std::exception& e) { + // 错误处理:在主线程中显示错误消息 + g_idle_add([](gpointer data) -> gboolean { + auto* error_data = static_cast*>(data); + error_data->first->append_message("错误: " + error_data->second); + delete error_data; + return FALSE; // 只执行一次 + }, new std::pair(this, e.what())); + } + }).detach(); // 分离线程,让它独立运行 + } + +private: + static void on_send_clicked(GtkButton* button, gpointer user_data) { + auto* self = static_cast(user_data); + std::string text = self->get_input_text(); + if (!text.empty()) { + // 用户消息不需要Markdown转换,直接显示 + self->append_message("You: " + text); + self->clear_input(); + self->send_to_llm(text); + } + } + + static void on_entry_activate(GtkEntry* entry, gpointer user_data) { + auto* self = static_cast(user_data); + std::string text = self->get_input_text(); + if (!text.empty()) { + // 用户消息不需要Markdown转换,直接显示 + self->append_message("You: " + text); + self->clear_input(); + self->send_to_llm(text); + } + } + + GtkWindow* window; + GtkTextView* text_view; + GtkEntry* entry; + GtkButton* send_button; + + std::unique_ptr llm_client; + std::unique_ptr markdown_converter; + std::vector> chat_history; + std::string current_response; +}; + +/** + * @brief 运行简单的聊天窗口示例 + */ +void run_simple_chat_example() { + // 标准GTK的gtk_init返回void,不是bool + gtk_init(nullptr, nullptr); + + SimpleChatWindow window; + window.show(); + + gtk_main(); +} + +} // namespace pmc::gui::example + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "用法: " << argv[0] << " " << std::endl; + std::cerr << "命令: chat - 运行聊天窗口" << std::endl; + return 1; + } + + std::string command = argv[1]; + + if (command == "chat") { + pmc::gui::example::run_simple_chat_example(); + } else { + std::cerr << "未知命令: " << command << std::endl; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/lci/src/mainLci.cpp b/lci/src/mainLci.cpp new file mode 100644 index 0000000..7271f33 --- /dev/null +++ b/lci/src/mainLci.cpp @@ -0,0 +1,740 @@ +/* + * 并发机器(进程托管) + * + * FIXME: 尽量使用大众的库(boost) + */ +#include +#include +#include +#include +#include +#include /* 注册信号以优雅退出 */ +#include +#include +#include +#include +#include "logs/Logger.hpp" +#include "th/Thread.hpp" /* 可控线程类 */ +#include "th/Cv_wait.hpp" /* 通过条件变量进行等待 */ +#include "th/ITask.hpp" +#include "lci/Term.h" +#include "lci/Rectangle.h" +#include "lci/Drawable.h" +#include "lci/FrameBuf.h" +#include "lci/PoolArea.h" +#include "lci/ShapeArea.h" +#include "lci/CharShape.h" +#include "lci/Color.h" +#include "lci/图层区域.hpp" +#include "hd/mem.h" +#include "hd/cpu.h" + +/* lci 临时 */ +#include "lci/p_th.h" + +/* 中文模式 Chinese Mode */ +#include "cn/中文化.hpp" +/*-------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------*/ +namespace po = boost::program_options; + +/* 版本号 */ +static const char *PMC_VERSION = "0.4.x"; +/* 条件变量的等待机制让主程序能够等待中断信号的产生 */ +qing::Cv_wait cv = qing::Cv_wait(); + + + + + + +/* 捕获中断信号,结束线程并且退出 */ +void signalHandler(int signum) +{ + LOG_INFO("收到终止信号,准备停止服务器..."); + cv.WakeCv(); /* 唤醒条件信号即关闭程序 */ +} + +/*----SIGNAL ACTION----*/ +// +/* 对信号的响应行为 */ +void set_signal_action() { + + /* 该结构体用于描述信号的处理方式 */ + struct sigaction sa; + + + /* 绑定处理函数 */ + sa.sa_handler = signalHandler; + + /* + * 清空并初始化一个信号掩码集 --------------> 用于指定哪些信号在当前或即将执行的信号处理函数期间应该被阻塞(位掩码) + * “在执行这个信号处理函数时,除了当前正在处理的信号外,还希望临时阻塞其他某些信号” + * 不阻塞其他信号 + * 确保在执行期间没有其他特定的信号会被阻塞 + * 防止处理过程中被其他信号中断 + */ + sigemptyset(&sa.sa_mask); + + /* + * 用于设置信号处理函数的行为特性。 + * 0 - 默认选项 + * SA_NODEFER - 表示在执行信号处理函数时不允许其他信号被阻塞 + * SA_RESETHAND - 在信号处理函数返回后,将信号的处理方式重置为默认 + * SA_NOCLDSTOP - 对于子进程发送的停止信号,父进程不会停止。 + * SA_NOCLDWAIT - 父进程在等待等待子进程终止时不会因为子进程发送信号而收到通知 + * SA_SIGINFO - 使用扩展的信号信息,信号处理函数接收三个参数而不是一个 + */ + sa.sa_flags = 0; + + /* 注册信号 */ + sigaction(SIGINT, &sa, nullptr); /* Ctrl + C */ + sigaction(SIGTERM, &sa, nullptr); /* kill 或 systemctl stop */ + +} + +/*-------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------*/ + +#define TARGET_TTY 8 /* FIXME: 目标端口设置为参数传入 */ +#define ORIGIN_TTY 1 +namespace qing { +class LciTask: public ITask{ +public: + LciTask(/*const std::string& cmd, const int masterfd, const int command_pipe_send, */const int rotate, const int fontsize) + : /*exec_cmd(cmd), masterfd(masterfd), command_pipe_send(command_pipe_send),*/ + rotate(rotate), fontsize(fontsize) { + /* 初始化线程 */ + init_thread(); + } + + /* 停止线程 */ + void stop() override { + th->WaitClose(); + } + + /* 唤醒线程 */ + void start() override{ + th->Activate(); /* 对于已经关闭的进程 */ + th->WaitStart(); + } + + /* 线程是否在运行 */ + bool isRunning() override { + auto stat = th->check(); + return stat == Fsm::Stat::START; + } + +private: + + std::string exec_cmd; + //int command_pipe_send; + //int masterfd; + + int rotate; + int fontsize; + bool shift = false; + + std::unique_ptr th; /* 持有线程类 */ + std::unique_ptr term; /* 终端代理 */ + std::unique_ptr fb; + //std::unique_ptr kbe; + //std::unique_ptr printTask; + std::unique_ptr poolArea; /* 持有控件池 */ + //std::shared_ptr cmd; + int table_w; + int table_h; + std::shared_ptr tableArea; + std::vector cpu_vec; + std::vector mem_vec; + + + /* + * 设置终端的大小 + * + * 传入一个控制台设备文件的文件描述符,以及你要设定的行列值 + */ + //void set_terminal_size(int fd, int rows, int cols) { + // struct winsize sz = { .ws_row = (unsigned short)rows, .ws_col = (unsigned short)cols }; + // if (ioctl(fd, TIOCSWINSZ, &sz) == -1) { + // throw std::runtime_error("ioctl"); + // } + //} + + /*---------------------*/ + void init_thread(); + +}; +} + + +int lci_init(po::variables_map&); + +/* 任务池 */ +auto taskPool = std::vector>{}; + + +/* 入口函数 */ +int main(int argc, char** argv) { + + /*------------------------------*/ + /* 设置信号行为*/ + set_signal_action(); + + /*------------------------------*/ + /* 解析命令行参数 */ + po::options_description desc("pmc subsystem start options"); + desc.add_options() /* 定义选项 */ + ("help,h", "display help message") + ("version,v", "display version message") + ("lci", "run lci task") + /*****************/ + //("exec", po::value(), "lci module run commandline") + ("rotate", po::value()->default_value(0), "lci module rotate mode(0~3)") + ("fontsize", po::value()->default_value(18), "lci module fontsize option"); + + /* 参数变量映射关系 */ + po::variables_map vm; + + try { /* 开始解析 */ + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); + if (vm.count("help")) { + std::cout << desc << std::endl; + return 0; + } + if (vm.count("version")) { + std::cout << PMC_VERSION << std::endl; + return 0; + } + } + + catch (const std::exception& e) { + std::cerr << "ERROR: " << e.what() << std::endl; + std::cerr << "using --help to check options message" << std::endl; + throw e; + } + + + + /* LINUX CHINESE INTERF */ + if (vm.count("lci")) { + + if (!(vm.count("rotate") && vm.count("fontsize")/* && vm.count("exec")*/)) + throw std::runtime_error( + "您进入了lci模块,请输入以下选项 --rotate, --fontsize"); + + lci_init(vm); + } + + /* + * 等待 + * + * 触发器在信号处理那里 + */ + cv.Wait(); + + /* 主线程被唤醒 */ + for (auto& task: taskPool) { + task->stop(); + } +} + + +/* 初始化PMC并发机器 */ +int lci_init(po::variables_map& vm) { + + //int masterfd; /* 控制台文件描述符 */ + /* 在运行到多线程之前,进行fork() */ + //if ((masterfd = posix_openpt(O_RDWR)) == -1) { /* 打开一个新的虚拟控制台 */ + // throw std::runtime_error("posix_openpt"); + //} + + /*--------------------------------------------------*/ + /* 创建两个管道:一个用于发送命令,一个用于接收输出 */ + //int command_pipe[2]; /* [0] 是读端,[1] 是写端 */ + //if (pipe(command_pipe) == -1) + // throw std::runtime_error( + // "pipe:" + std::string(strerror(errno)) + // ); + /* 创建成功 */ + /*--------------------------------------------------*/ + +// if (grantpt(masterfd) != 0 || unlockpt(masterfd) != 0) { /* 解锁虚拟控制台? */ +// close(masterfd); +// throw std::runtime_error("grantpt/unlockpt"); /* FIXME: 输出错误字符串 */ +// } + +// char *slave_name = ptsname(masterfd); /* 获取虚拟控制台pty的名字 */ +// if (slave_name == NULL) { +// close(masterfd); +// throw std::runtime_error("ptsname"); +// } + +// LOG_INFO( +// std::string("新的虚拟终端运行在 ") + slave_name +// ); + + + + + /* Fork中间子进程 */ +// pid_t pid = fork(); + + /* pid为-1通常意味着分叉失败 */ +// if (pid == -1) +// throw std::runtime_error( +// std::string("fork") + std::string(strerror(errno)) +// ); + + /* 如果pid为0表示这是子进程 */ +// else if (pid == 0) { +// LOG_INFO("LCI subprocess is running..."); +// close(command_pipe[1]); /* 关闭管道写端 */ +// int slavefd = open(slave_name, O_RDWR); +// if (slavefd == -1) +// throw std::runtime_error( +// std::string("open slave pty") + std::string(strerror(errno)) +// ); + + //runIntermediateProcess(command_pipe[0], slavefd); +// int command_pipe_read = command_pipe[0]; +// char buffer[1024]; +// ssize_t nread; +// while (true) { +// memset(buffer, 0, sizeof(buffer)); /* 清空缓冲区 */ +// nread = read(command_pipe_read, buffer, sizeof(buffer) - 1); /* 从命令管道读取数据 */ +// if (nread <= 0) break; /* 没有数据可读,退出循环 */ + + /* 查找命令的结束位置 */ + /* FIXME: 更换结束符号,这样我们可以运行一整个脚本 */ +// char* end_of_cmd = strchr(buffer, '\n'); +// if (end_of_cmd != nullptr) +// *end_of_cmd = '\0'; +// else +// continue; /* 没找到结束符号,继续读取 */ + + /* 检查是否受到结束信号 */ +// if (strcmp(buffer, "END") == 0) +// break; + + /* 输入输出重定向 */ +// if (dup2(slavefd, STDIN_FILENO) != STDIN_FILENO +// || dup2(slavefd, STDOUT_FILENO) != STDOUT_FILENO +// || dup2(slavefd, STDERR_FILENO) != STDERR_FILENO) +// throw std::runtime_error( +// std::string("dup2") // + +// ); + + /* ?*/ +// if (slavefd > STDERR_FILENO) +// close(slavefd); + + /* 执行命令 */ +// execlp("/bin/sh", "/bin/sh", "-c", buffer, nullptr); + + /* 如果能运行到这里表示进程切换异常 */ +// throw std::runtime_error( +// std::string("execlp") // + +// ); + +// } + +// close(command_pipe[0]); +// _exit(0); +// } + + /* 返回其他正数表示这是主进程 */ +// else { + +// LOG_INFO("LCI main thread is running..."); + + + + auto lciTask = std::make_shared( +// vm["exec"].as(), +// masterfd, command_pipe[1], + vm["rotate"].as(), + vm["fontsize"].as() + ); + lciTask->start(); + taskPool.push_back(lciTask); + + + /* 等待中间子进程终止 */ +// waitpid(pid, nullptr, 0); + + /* 将管道移动到分支外面 */ +// close(command_pipe[0]); // 关闭读端 +// close(command_pipe[1]); + +// close(masterfd); /* 关闭虚拟终端 */ + +// } + + return 0; +} + +namespace qing { + + void LciTask::init_thread() { + + /* 静止事件 */ + f_t StopEvent = [](Thread& th) { + th.suspend(); + }; + + /* 启动事件 */ + f_t StartEvent = [this] (Thread& th) { + + this->term = std::make_unique(TARGET_TTY, ORIGIN_TTY); + this->fb = std::make_unique("/dev/fb0", rotate, fontsize); + //this->kbe = std::make_unique(); + + Rectangle rect = fb->get_size(); /* 屏幕大小 */ + //set_terminal_size(masterfd, rect.h / (fontsize/2), rect.w / (fontsize/2) - 1); + + + //int cmd_margin_left = 50; + //int cmd_margin_top = 50; + //int cmd_w = (rect.w - 3*50) * 0.7; + //int cmd_h = (rect.h - 2*50) * 1.0; + + //int log_margin_left = 50 * 2 + cmd_w; + //int log_margin_top = 50; + //int log_w = (rect.w - 3 * 50) * 0.3; + //int log_h = (rect.h - 3 * 50) / 2; + + //int video_margin_left = 50 * 2 + cmd_w; /* 位置 */ + //int video_margin_top = log_margin_top + log_h + 50; + //int video_w = (rect.w - 3 * 50) * 0.3;/* 大小 */ + //int video_h = (rect.h - 3 * 50) / 2; + // + int top_margin_left = 10; + int top_margin_top = 10; + int top_w = rect.w - 2 * 10; + int top_h = 40; + + int main_margin_left = 10; + int main_margin_top = 10 + top_margin_top + top_h; + int main_w = rect.w - 2 * 10; + int main_h = rect.h - 4 * 10 - 2 * 40; + + int bottom_margin_left = 10; + int bottom_margin_top = 10 + main_margin_top + main_h; + int bottom_w = rect.w - 2 * 10; + int bottom_h = 40; + + + /* 容器区域 */ + poolArea = std::make_unique(fb.get(), rect.w, rect.h, 0, 0, rotate, fontsize); + + auto topArea = std::make_shared(poolArea.get(), top_w, top_h, top_margin_left, top_margin_top, rotate, fontsize); + poolArea->add("top", topArea); + //poolArea->flush(topArea.get(), true); + //auto topClr = green; + //topArea->rectangle_fill(0, 0, top_w, top_h, topClr); + + /* 顶部内容1 */ + auto topCtt_1 = std::make_shared (topArea.get(), top_w -10, top_h -10, 5, 5, rotate, fontsize); + topArea->add("content1", topCtt_1); + + /* 第一个字 */ + auto tctt1_c1 = std::make_shared(top_h-10, top_h-10, 0, 0, 'C'); + tctt1_c1->Draw(*topCtt_1, red); + /* 第二个字 */ + auto tctt1_c2 = std::make_shared(top_h-10, top_h-10, top_h-10, 0, 'P'); + tctt1_c2->Draw(*topCtt_1, red); + /* 第三个字 */ + auto tctt1_c3 = std::make_shared(top_h-10, top_h-10, 2*(top_h-10), 0, 'U'); + tctt1_c3->Draw(*topCtt_1, red); + + + auto bottomArea = std::make_shared(poolArea.get(), bottom_w, bottom_h, bottom_margin_left, bottom_margin_top, rotate, fontsize); + poolArea->add("bottom", bottomArea); + //poolArea->flush(bottomArea.get(), true); + //auto bottomClr = green; + //bottomArea->rectangle_fill(0, 0, bottom_w, bottom_h, bottomClr); + + auto mainArea = std::make_shared(poolArea.get(), main_w, main_h, main_margin_left, main_margin_top, rotate, fontsize); + poolArea->add("main", mainArea); + poolArea->flush(mainArea.get(), false); + auto mainClr = green; + mainArea->line(20, 20, 20, main_h-20, mainClr); + mainArea->line(21, 20, 21, main_h-20, mainClr); + mainArea->line(20, main_h-18, main_w-20, main_h-18, mainClr); + mainArea->line(20, main_h-19, main_w-20, main_h-19, mainClr); + + table_w = main_w - 41; + table_h = main_h - 39; + tableArea = std::make_shared (mainArea.get(), table_w, table_h, 22, 20, rotate, fontsize); + mainArea->add("table", tableArea); + mainArea->flush(tableArea.get(), false); + + /* NOTE: 子进程收到命令会执行 */ + //write(command_pipe_send, exec_cmd.c_str(), exec_cmd.size()); + //const char* sig = "\n"; + //write(command_pipe_send, sig, strlen(sig)); + + //this->printTask = std::make_unique( /* 创建打印线程 */ + // "Printer", + // masterfd, + // fb.get(), + // cmd.get() + //); + //printTask->wake(); + //printTask->WaitStart(10000); /* FIXME: 使用新版的线程类 */ + + th.run(); + }; + + + f_t LoopEvent = [this](Thread& th) { + + //try { + + // struct input_event ev = kbe->get(); /* 获取输入事件 */ + // std::cout << "Key Press:\t"; + // std::cout << ev.code << "," << ev.value << std::endl; + + // if (ev.type == EV_KEY && ev.value == 1 && ev.code == 1) + // { /* ESC 退出键 */ + // th.stop(); + // } + + // else if (ev.type == EV_KEY && ev.value == 1 && ev.code == 14) + // { /* DELETE 删除键 */ + //cmd->delete_input(); + //cmd->clearBox(); + //cmd->update_input(); + // } + + // else if (ev.type == EV_KEY && ev.value == 1 && ev.code == 28) + // { /* ENTER 回车 */ + //auto input = cmd->get_input_and_clear(); + //cmd->clearBox(); + //cmd->update_input(); + //if (input == "") { + // input = "\n"; + //} + //const char *msg = input.c_str(); + //write(masterfd, msg, input.length()); + // } + + // else if (ev.type == EV_KEY && ev.code == 42) + // { /* Shift */ + // shift = (ev.value) ? true : false; + // } + + // else if (ev.type == EV_KEY && ev.value == 1) + // { /* 其他有效按键 */ + + // char c = '\0'; /* 功能变换 */ + // switch (ev.code) { + // case 2: + // c = (shift) ? '!' : '1'; + // break; + // case 3: + // c = (shift) ? '@' : '2'; + // break; + // case 4: + // c = (shift) ? '#' : '3'; + // break; + // case 5: + // c = (shift) ? '$' : '4'; + // break; + // case 6: + // c = (shift) ? '%' : '5'; + // break; + // case 7: + // c = (shift) ? '^' : '6'; + // break; + // case 8: + // c = (shift) ? '&' : '7'; + // break; + // case 9: + // c = (shift) ? '*' : '8'; + // break; + // case 10: + // c = (shift) ? '(' : '9'; + // break; + // case 11: + // c = (shift) ? ')' : '0'; + // break; + // case KEY_A: + // c = (shift) ? 'A' : 'a'; + // break; + // case KEY_B: + // c = (shift) ? 'B' : 'b'; + // break; + // case KEY_C: + // c = (shift) ? 'C' : 'c'; + // break; + // case KEY_D: + // c = (shift) ? 'D' : 'd'; + // break; + // case KEY_E: + // c = (shift) ? 'E' : 'e'; + // break; + // case KEY_F: + // c = (shift) ? 'F' : 'f'; + // break; + // case KEY_G: + // c = (shift) ? 'G' : 'g'; + // break; + // case KEY_H: + // c = (shift) ? 'H' : 'h'; + // break; + // case KEY_I: + // c = (shift) ? 'I' : 'i'; + // break; + // case KEY_J: + // c = (shift) ? 'J' : 'j'; + // break; + // case KEY_K: + // c = (shift) ? 'K' : 'k'; + // break; + // case KEY_L: + // c = (shift) ? 'L' : 'l'; + // break; + // case KEY_M: + // c = (shift) ? 'M' : 'm'; + // break; + // case KEY_N: + // c = (shift) ? 'N' : 'n'; + // break; + // case KEY_O: + // c = (shift) ? 'O' : 'o'; + // break; + // case KEY_P: + // c = (shift) ? 'P' : 'p'; + // break; + // case KEY_Q: + // c = (shift) ? 'Q' : 'q'; + // break; + // case KEY_R: + // c = (shift) ? 'R' : 'r'; + // break; + // case KEY_S: + // c = (shift) ? 'S' : 's'; + // break; + // case KEY_T: + // c = (shift) ? 'T' : 't'; + // break; + // case KEY_U: + // c = (shift) ? 'U' : 'u'; + // break; + // case KEY_V: + // c = (shift) ? 'V' : 'v'; + // break; + // case KEY_W: + // c = (shift) ? 'W' : 'w'; + // break; + // case KEY_X: + // c = (shift) ? 'X' : 'x'; + // break; + // case KEY_Y: + // c = (shift) ? 'Y' : 'y'; + // break; + // case KEY_Z: + // c = (shift) ? 'Z' : 'z'; + // break; + // case KEY_SPACE: + // c = ' '; + // break; + // case 51: + // c = (shift) ? '<' : ','; + // break; + // case 52: + // c = (shift) ? '>' : '.'; + // break; + // case 53: + // c = (shift) ? '?' : '/'; + // break; + // case 12: + // c = (shift) ? '_' : '-'; + // break; + // case 43: + // c = (shift) ? '|' : '\\'; + // break; + + // } + + // if (c != '\0') /* 如果点击了有效按键 */ + // { + //cmd->Input(c); + //cmd->clearBox(); + //cmd->update_input(); + // } + + // } else if (ev.type == EV_REL && ev.code == REL_X) { /* 这两个是鼠标移动事件 */ + // std::cout << "X:\t\t" << ev.value << std::endl; + // } else if (ev.type == EV_REL && ev.code == REL_Y) { + // std::cout << "Y:\t\t" << ev.value << std::endl; + // } + + //} catch( KeyboardEvent::NoEvent _ ) { + // ; + //} + + + MemoryInfo mem = getMemoryUsageWithSysinfo(); + mem_vec.push_back(mem.usageRate); + if (mem_vec.size() > 40) { + mem_vec.erase(mem_vec.begin()); + } + + CpuMonitor monitor; + double cpu_usage = monitor.get_cpu_usage(); + cpu_vec.push_back(cpu_usage); + if (cpu_vec.size() > 40) { + cpu_vec.erase(cpu_vec.begin()); + } + + + auto bg = black; + tableArea->rectangle_fill(0, 0, table_w, table_h, bg); + + float single_w = (float)(table_w-1) / 40.0; + float single_h = (float)(table_h-1) / 100.0; + + for (int i = 0; i < mem_vec.size() - 1; ++i) { + int x0 = i * single_w; + int y0 = (100.0 - mem_vec[i]) * single_h; + int x1 = x0 + single_w; + int y1 = (100.0 - mem_vec[i+1]) * single_h; + auto fg = blue; + tableArea->line(x0, y0, x1, y1, fg); + } + + for (int i = 0; i < cpu_vec.size() - 1; ++i) { + int x0 = i * single_w; + int y0 = (100.0 - cpu_vec[i]) * single_h; + int x1 = x0 + single_w; + int y1 = (100.0 - cpu_vec[i+1]) * single_h; + auto fg = red; + tableArea->line(x0, y0, x1, y1, fg); + } + + + usleep(1000000); + }; + + + f_t ClearEvent = [this](Thread& th) + { + //printTask.reset(); + //cmd.reset(); + poolArea.reset(); + tableArea.reset(); + term.reset(); + }; + + this->th = std::make_unique( + StopEvent, StartEvent, LoopEvent, ClearEvent + ); + + + + } +} diff --git a/lci/src/mainMybot.cpp b/lci/src/mainMybot.cpp new file mode 100644 index 0000000..aec6506 --- /dev/null +++ b/lci/src/mainMybot.cpp @@ -0,0 +1,526 @@ +/* + * 并发机器(进程托管) + * + * FIXME: 尽量使用大众的库(boost) + */ +#define HTTPLIB_COMPILE /* 无SSL需求,优先选这个;有SSL需求替换为 #define HTTPLIB_OPENSSL_SUPPORT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* 注册信号以优雅退出 */ +#include +#include "logs/Logger.hpp" +#include "th/Thread.hpp" /* 可控线程类 */ +#include "th/Cv_wait.hpp" /* 通过条件变量进行等待 */ +#include "th/ITask.hpp" +#include "nn/NNBuilder.hpp" +#include "hd/Robot.hpp" + +/* lci 临时 */ +#include "lci/p_th.h" + +/* 中文模式 Chinese Mode */ +#include "cn/中文化.hpp" +/*-------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------*/ +namespace po = boost::program_options; + +/* 版本号 */ +static const char *PMC_VERSION = "0.4.x"; +/* 条件变量的等待机制让主程序能够等待中断信号的产生 */ +qing::Cv_wait cv = qing::Cv_wait(); + + +/* + * Http Server Task + * 超文本传输服务器任务 + */ +namespace qing{ +class HttpTask: public qing::ITask { +public: + /* 调用回调函数,设置http服务器 */ + HttpTask(const std::string& addr, const int port, std::function f) + : http_addr(addr), http_port(port), callback(f) { + + init_thread(); + } + + /* 任务不可复制 */ + HttpTask(const HttpTask& ) = delete; + + /* 关闭线程、关闭服务 */ + ~HttpTask() { + if (http) http->stop(); + if (th) th->WaitClose(); + } + + + /* 从外部停止任务 */ + void stop() override { + if (http) http->stop(); + th->WaitClose(); + } + + /* 唤醒线程 */ + void start() override { + th->Activate(); /* 对于已关闭的线程 */ + th->WaitStart(); + } + + /* 是否在运行 */ + bool isRunning() override { + auto stat = th->check(); + return stat == Fsm::Stat::START + || stat == Fsm::Stat::RUNNING; + } +private: + std::unique_ptr th; + std::function callback; + std::unique_ptr http; + std::string http_addr; + int http_port; + + /* 初始化线程 */ + void init_thread(); +}; //HttpTask +} // qing + + +namespace qing{ +static const std::string ADDR = std::string{"127.0.0.1"}; /* 只监听本机 */ +static const int PORT = 9203; +using nnl = NeuralNetwork; +class ParserTask: public HttpTask { /* 动作解析器 */ +public: + ParserTask(); + ParserTask(const ParserTask&) = delete; + + /* 解析器异常 */ + class ParserException: public std::exception{ + protected: + std::string message; + public: + explicit ParserException(const std::string& msg): message(msg) {} + const char* what() const noexcept override { + return message.c_str(); + } + }; + + /* 没有消息体异常 */ + class NoRequestBodyException: public ParserException { + public: + NoRequestBodyException(const std::string& msg): ParserException(msg) {} + }; + +private: + NNBuilder nn; + std::mutex mtx; + std::vector>> pool; /* 角度数据 - 样品池 */ + std::vector> averages; /* 角度数据 - 平均值 */ + /* 归一化 */ + static float normalize_pitch(float pitch) { + return (pitch + 180) / (2 * 180); /* FIXME: 方向反了 */ + } + /* 归一化 */ + static float normalize_roll(float roll) { + return (roll + 180) / (2 * 180); /* 观测值 */ + } + /* 计算误差 */ + static float calc_err(float nor_pitch, float pitch1) { + + return (nor_pitch > 0) /* 计算误差 */ + ? ((pitch1 > 0) ? nor_pitch - pitch1 : nor_pitch + pitch1 ) + : ((pitch1 < 0) ? pitch1 - nor_pitch : -(nor_pitch + pitch1)); + } + /* FIXME: 使用类封装采样数据 */ + static std::vector> calc_aves(std::vector>>& samples) { + std::vector> a_r{}; /* actions of average result FIXME: 长度必须一致 */ + for (const std::vector>& onetim: samples) { /* one time */ + auto first_action = onetim[0]; + std::vector grp_r(first_action.size()); /* group of averate result */ + for (const std::vector& action: onetim) { + for (size_t i_nora = 0; i_nora < action.size(); ++i_nora) { /* item of normalized angle*/ + grp_r[i_nora] += action[i_nora]; + } + } + auto length = onetim.size(); + for (auto& i_r: grp_r) { /* item of average result */ + i_r /= length; + } + a_r.push_back(grp_r); + } + return a_r; + } + + /* 利用神经网络推理微调角度 */ + std::vector> run_infer( + std::string script, float infer_freq, float train_discount, std::vector>& averages); +}; +} + + + +/* 捕获中断信号,结束线程并且退出 */ +void signalHandler(int signum) +{ + LOG_INFO("收到终止信号,准备停止服务器..."); + cv.WakeCv(); /* 唤醒条件信号即关闭程序 */ +} + +/*----SIGNAL ACTION----*/ +// +/* 对信号的响应行为 */ +void set_signal_action() { + + /* 该结构体用于描述信号的处理方式 */ + struct sigaction sa; + + + /* 绑定处理函数 */ + sa.sa_handler = signalHandler; + + /* + * 清空并初始化一个信号掩码集 --------------> 用于指定哪些信号在当前或即将执行的信号处理函数期间应该被阻塞(位掩码) + * “在执行这个信号处理函数时,除了当前正在处理的信号外,还希望临时阻塞其他某些信号” + * 不阻塞其他信号 + * 确保在执行期间没有其他特定的信号会被阻塞 + * 防止处理过程中被其他信号中断 + */ + sigemptyset(&sa.sa_mask); + + /* + * 用于设置信号处理函数的行为特性。 + * 0 - 默认选项 + * SA_NODEFER - 表示在执行信号处理函数时不允许其他信号被阻塞 + * SA_RESETHAND - 在信号处理函数返回后,将信号的处理方式重置为默认 + * SA_NOCLDSTOP - 对于子进程发送的停止信号,父进程不会停止。 + * SA_NOCLDWAIT - 父进程在等待等待子进程终止时不会因为子进程发送信号而收到通知 + * SA_SIGINFO - 使用扩展的信号信息,信号处理函数接收三个参数而不是一个 + */ + sa.sa_flags = 0; + + /* 注册信号 */ + sigaction(SIGINT, &sa, nullptr); /* Ctrl + C */ + sigaction(SIGTERM, &sa, nullptr); /* kill 或 systemctl stop */ + +} + +/*-------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------*/ +/* 初始化pmc并行机器 */ +int pmc_init(po::variables_map&); + +/* 任务池 */ +auto taskPool = std::vector>{}; + +/*----------------------------------------*/ + +/* FIXME: 它们没有清理 + * FIXME: 传入路径、地址 */ +void try_start_i2c_and_camera() +try { + I2C_初始化(); + PCA9685_初始化(); + MPU6050_初始化(); + + /* 初始化相机 */ + Camera_init(640, 480); + Camera_check(); + Camera_setVideoFormat(); + Camera_reqBuf(); + Camera_setup(); + //Camera_run(); +} +catch (std::runtime_error& exp) +{ + std::cout << exp.what() << std::endl; +} + +/* 入口函数 */ +int main(int argc, char** argv) { + + /*------------------------------*/ + /* 设置信号行为*/ + set_signal_action(); + + /*------------------------------*/ + /* 解析命令行参数 */ + po::options_description desc("pmc subsystem start options"); + desc.add_options() /* 定义选项 */ + ("help,h", "display help message") + ("version,v", "display version message") + ("mybot", "run mybot task"); + + /* 参数变量映射关系 */ + po::variables_map vm; + + try { /* 开始解析 */ + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); + if (vm.count("help")) { /* 获取帮助页面 */ + std::cout << desc << std::endl; + return 0; + } + if (vm.count("version")) { /* 获取版本信息 */ + std::cout << PMC_VERSION << std::endl; + return 0; + } + } + + catch (const std::exception& e) { + std::cerr << "ERROR: " << e.what() << std::endl; + std::cerr << "using --help to check options message" << std::endl; + throw e; + } + + /*----------------------------------------*/ + if (vm.count("mybot")) { + try_start_i2c_and_camera(); /* 启动硬件模组 FIXME: 没有释放资源 */ + auto task = std::make_shared(); + task->start(); + taskPool.push_back(task); + } + + + + /* + * 等待 + * + * 触发器在信号处理那里 + */ + cv.Wait(); + + /* 主线程被唤醒 */ + for (auto& task: taskPool) { + task->stop(); + } +} + + +namespace qing { + +/* 构造函数,主要是需要传入一个http构造回调 */ +ParserTask::ParserTask(): HttpTask(ADDR, PORT, [this](Http& http) { + + + /* FIXME: 传入模型路径 */ + const char* target = "../public/models/1.txt"; + if (std::filesystem::exists(target)) { + std::cout << "load model data from file..." << std::endl; + std::ifstream file; + file.open(target); + nn.load(file); + file.close(); + } + else { /* FIXME: 单独放在一个函数里 */ + std::cout << "create model data rand..." << std::endl; + + auto layer1 = nnl::Create_in_Factory(18, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU); + auto layer2 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU); + auto layer3 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU); + auto layer4 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU); + auto layer5 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU); + auto layer6 = nnl::Create_in_Factory(36, 16, 0.01, nnl::ActivationFunc::Sigmoid); + + nn.add(layer1); + nn.add(layer2); + nn.add(layer3); + nn.add(layer4); + nn.add(layer5); + nn.add(layer6); + + /* 储存模型 */ + std::ofstream file; + file.open("../public/models/1.txt"); + if (!file) { + std::cout << "Open Model failed" << std::endl; + } + else { + nn.save(file); + file.close(); + } + } + + nn.print_shape(); + + + /* FIXME:当消息体为空时抛出异常即可 */ + http.post("/api/v1/mybot-sample", [this](const httplib::Request& req, httplib::Response& res) { + try{ + if (req.body.empty()) + throw NoRequestBodyException("/api/v1/mybot-sample"); + mtx.lock(); + auto r = run_infer(req.body, 0.0, 0.0, averages); /* 推理结果无效化 */ + pool.push_back(r); + averages = calc_aves(pool); + mtx.unlock(); + res.set_content("OK", "text/plain"); + } + catch (NoRequestBodyException& exp) { + res.set_content("no request body", "text/plain"); + res.status =400; + } + catch (std::exception& exp) { + res.set_content(exp.what(), "text/plain"); + res.status =400; + } + + }); + + /* FIXME: 需要先取得模板数据 */ + http.post("/api/v1/mybot-nn", [this](const httplib::Request& req, httplib::Response& res) { + try{ + if (req.body.empty()) + throw NoRequestBodyException("/api/v1/mybot-nn"); + mtx.lock(); + auto r =run_infer(req.body, 0.1, 10.0, averages); + for (auto& v: r) { /* 输出运行结果 */ + std::cout << "("; + for (auto& i: v) { + std::cout << i; + std::cout << ","; + } + std::cout << ")\t"; + std::cout << std::endl; + } + mtx.unlock(); + res.set_content("OK", "text/plain"); + } + catch (NoRequestBodyException& exp) { + res.set_content("no request body", "text/plain"); + res.status =400; + } + catch (std::exception& exp) { + res.set_content(exp.what(), "text/plain"); + res.status =400; + } + }); + +}) {} + +/*--------------------------------------------------------------------*/ +/*--------------------------------------------------------------------*/ +/* 神经网络推理 */ +std::vector> ParserTask::run_infer(std::string script, float infer_freq, float train_discount, std::vector>& averages) { + auto parser = ActParser(); + parser.fromStr(script); + auto bot = parser.get(); + auto res = std::vector>{}; + auto all_count = 0; + while (true) { + try{ + std::cout << "------------------------------"<< std::endl; + MPU6050_传感器数据 data = MPU6050_读传感器(); + float pitch, roll; + MPU6050_计算角度(data, pitch, roll); + auto nor_pitch = normalize_pitch(pitch); + auto nor_roll = normalize_roll(roll); + auto act = parser.now(); /* 获取一个动作 */ + auto x = std::vector(); /* 创建输入 */ + x.push_back(nor_pitch); + x.push_back(nor_roll); + auto i{0}; + for (; i < act.size()-1; ++i) { /* 跳过了延时数值 */ + float a; + if (bot[i] == 0) { a = 0; } /* 空舵机位 */ + else { a = (float)act[i] / bot[i]; } + x.push_back(a); + } + auto x1 = x; + auto o = nn.forward(x1); /* 前向反馈 */ + auto j{2}; + auto new_act = std::vector (); + unsigned seed = std::chrono::system_clock::now().time_since_epoch().count(); + std::mt19937 generator(seed); + std::uniform_real_distribution distribution(-0.5, 0.5); // 1-100 均匀分布 + for (; j http=std::make_unique(http_addr, http_port); + + this->callback(*http); + + if (th.check() == Fsm::Stat::START) { /* 只跑一次 */ + th.run(); /* 标记为启动 */ + http->run(); /* 这个会堵塞线程以进行监听 */ + } + + /* HTTP服务器异常关闭 */ + if (th.check() == Fsm::Stat::RUNNING) { + th.stop(); + } + + + }; + + f_t LoopEvent = [](Thread& _) { + throw std::runtime_error("Task not support this Event"); + }; + + f_t ClearEvent = [this](Thread& _) { + if (this->http) + this->http.reset(); /* */ + }; + + th = std::make_unique (StopEvent, WakeEvent, LoopEvent, ClearEvent); + } + +} diff --git a/lci/src/mainNn.cpp b/lci/src/mainNn.cpp new file mode 100644 index 0000000..125cc11 --- /dev/null +++ b/lci/src/mainNn.cpp @@ -0,0 +1,188 @@ +#include +#include +#include +#include +#include +#include +#include +//#include +#include "hd/Camera.h" +#include "nn/ImageLoader.hpp" +#include "nn/ConvLayer.hpp" +#include "nn/PoolingLayer.hpp" +#include "nn/FlattenLayer.hpp" +#include "nn/CNNBuilder.hpp" +#include "nn/NNBuilder.hpp" +#include "nn/NeuralNetwork.hpp" + +using namespace qing; + + +// 定义颜色用于不同的人脸框 +const unsigned char colors[][3] = { + {255, 0, 0}, // 红色 + {0, 255, 0}, // 绿色 + {0, 0, 255}, // 蓝色 + {255, 255, 0}, // 黄色 + {255, 0, 255} // 紫色 +}; + +void take_a_photo(const char* path); +int run_facerec(const char* in, const char* out); + +// 主函数 +int main(int argc, char* argv[]) { + + ImageLoader image_loader(3, 480, 640, true, false); + + ConvLayer conv1(3, 16, 3, 1, 1); + PoolingLayer pool1(2, 2, 2, 0, true); + ConvLayer conv2(16, 32, 3, 1, 1); + PoolingLayer pool2(2, 2, 2, 0, true); + ConvLayer conv3(32, 64, 3, 1, 1); + PoolingLayer pool3(2, 2, 2, 0, true); + ConvLayer conv4(64, 128, 3, 1, 1); + PoolingLayer pool4(2, 2, 2, 0, true); + ConvLayer conv5(128, 256, 3, 1, 1); + PoolingLayer pool5(2, 2, 2, 0, true); + ConvLayer conv6(256, 512, 3, 1, 1); + PoolingLayer pool6(2, 2, 2, 0, true); + FlattenLayer flatten; + + CNNBuilder cnn; + int in_channels = 3; + int in_h = 480; + int in_w = 640; + int batch_size = 1; + + cnn.add_conv_layer(conv1, in_channels, in_h, in_w); + cnn.add_pool_layer(pool1, 16, in_h, in_w); + cnn.add_conv_layer(conv2, 16, in_h/2, in_w/2); + cnn.add_pool_layer(pool2, 32, in_h/2, in_w/2); + cnn.add_conv_layer(conv3, 32, in_h/4, in_w/4); + cnn.add_pool_layer(pool3, 64, in_h/4, in_w/4); + cnn.add_conv_layer(conv4, 64, in_h/8, in_w/8); + cnn.add_pool_layer(pool4, 128, in_h/8, in_w/8); + cnn.add_conv_layer(conv5, 128, in_h/16, in_w/16); + cnn.add_pool_layer(pool5, 256, in_h/16, in_w/16); + cnn.add_conv_layer(conv6, 256, in_h/32, in_w/32); + cnn.add_pool_layer(pool6, 512, in_h/32, in_w/32); + cnn.add_flatten_layer(flatten, 512, 7, 10); + + cnn.print_architecture(); + + NNBuilder nn; + using nnl = NeuralNetwork; + + auto layer0 = nnl::Create_in_Factory(15360, 6500,0.5, nnl::ActivationFunc::Leaky_ReLU); + auto layer1 = nnl::Create_in_Factory(6500, 1500, 0.45, nnl::ActivationFunc::Leaky_ReLU); + auto layer2 = nnl::Create_in_Factory(1500, 300, 0.40, nnl::ActivationFunc::Leaky_ReLU); + auto layer3 = nnl::Create_in_Factory(300, 70, 0.35, nnl::ActivationFunc::Leaky_ReLU); + auto layer4 = nnl::Create_in_Factory(70, 5, 0.30, nnl::ActivationFunc::Sigmoid); + + nn.add(layer0); + nn.add(layer1); + nn.add(layer2); + nn.add(layer3); + nn.add(layer4); + + nn.print_shape(); + + + //Camera_init(640, 480); + bool flag = true; + while (flag) { + + + //std::string imgpath = std::string("./dataSet/" + std::to_string(time(NULL)) + ".jpeg"); + //std::string txtpath =std::string("./dataSet/" + std::to_string(time(NULL)) + ".txt"); + + //bool flag; + //auto img = get_frame_from_camera(flag); + //img.save(imgpath.c_str()); + + //if (run_facerec(imgpath.c_str(), txtpath.c_str())) + // throw std::runtime_error("facerec"); + + std::string data_root = "./dataSet"; + namespace fs = std::filesystem; + for (const auto& entry : fs::recursive_directory_iterator(data_root)) { + if (!fs::is_regular_file(entry.status())) + continue; + + std::string basename = entry.path().stem().string(); + std::string txtpath = data_root + "/" + basename + ".txt"; + std::string imgpath = data_root + "/" + basename+".jpeg"; + auto img = cimg_library::CImg(imgpath.c_str()); + + auto image_data = image_loader.load_from_cimg(img, true); + if (image_data.empty()) { + std::cerr << "Failed to load image!" << std::endl; + break; + } + + /* 前向传播 */ + auto output = cnn.forward(image_data, batch_size, true); + if (!output.empty()) { + std::cout << "Forward pass successful!" << std::endl; + std::cout << "Input size: " << image_data.size() << std::endl; + std::cout << "Output size: " << output.size() << std::endl; + + // 计算输出统计信息 + float sum = 0.0f, max_val = output[0], min_val = output[0]; + for (float val : output) { + sum += val; + if (val > max_val) max_val = val; + if (val < min_val) min_val = val; + } + + std::cout << "Output statistics:" << std::endl; + std::cout << " Mean: " << sum / output.size() << std::endl; + std::cout << " Min: " << min_val << std::endl; + std::cout << " Max: " << max_val << std::endl; + } + + std::vector t(5); + std::ifstream file(txtpath); + if (!file) t[0] = 0; + else { + t[0] = 1; + for (int i=0; i<5; ++i) { + file >> t[i+1]; + std::cout << t[i+1] << " "; + } + } + std::cout<< std::endl; + auto r = nn.forward(output); + auto y = nn.cal_err(r, t); + + /* 不存在人脸时,位置信息不计入 */ + if (t[0] == 0) + for (int i =1; i< 5; i++) + y[i]= 0; + else { + for (auto i: y) { + std::cout << i << " "; + } + std::cout << std::endl; + + auto fix_img = img.draw_rectangle(t[4]*640,t[1]*480, t[2]*640,t[3]*480, colors[1], 1, ~0U); /* 抽象为函数 */ + fix_img = fix_img.draw_rectangle(r[4]*640,r[1]*480, r[2]*640,r[3]*480, colors[0], 1, ~0U); + std::string respath = "./resSet/" + basename+".jpeg"; + fix_img.save(respath.c_str()); + } + y = nn.backward(y); + y = cnn.backward(y); + } + } + + //Camera_close(); +} + +int run_facerec(const char* in, const char* out) { + auto cmd = std::string("python tools/facerec.py detect "); + cmd += in; + cmd += " --output "; + cmd += out; + return system(cmd.c_str()); +} \ No newline at end of file diff --git a/lci/src/mainParser.cpp b/lci/src/mainParser.cpp new file mode 100644 index 0000000..9c3a48f --- /dev/null +++ b/lci/src/mainParser.cpp @@ -0,0 +1,17 @@ +/* + * 该文件可以解析动作脚本(.bot)并且将角度映射到动作上 。 + * + * FIXME: 改为可以启动http服务器进行监听 + */ + +#include "Dashun.h" +#include "Dashun_linux.hpp" + +auto parser = qing::ActParser(); +int main(int argc, char** argv) { /* argc >= 1 */ + I2C_init(); + PCA9685_init(); + parser.fromStdin(); + //parser.fromStr(argv[1]); + parser.play(); +} diff --git a/lci/src/mainParserNn.cpp b/lci/src/mainParserNn.cpp new file mode 100644 index 0000000..a910e61 --- /dev/null +++ b/lci/src/mainParserNn.cpp @@ -0,0 +1,253 @@ +#include "Dashun.h" +#include "Dashun_linux.hpp" +#include "test_nn_20251207_0.hpp" +#include "test_nn_20251219_0.hpp" +#include +#include + + + +/////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////// +/* +* 适用于机器人的DRL(深度强化学习) +* +* 【方案1】 +* 蜘蛛机器人拥有10个舵机(4只胳膊、4只腿、2个摄像云台),它还有2个陀螺仪参数。 +* 将舵机目前的位置+陀螺仪参数传递给神经网络,输出10个舵机的概率值 +* 将概率值加上一个0~1之间的随机因子,获取最终输出 +* +* 每个舵机180度,即 10 * 180 = 1800 个参数 +* +* 【方案2】 +* +* 将蜘蛛机器人当前的陀螺仪读数,以及下一个动作的参数传递给模型 +* +* 教师数据根据下一个动作的陀螺仪偏差决定 +*/ + +float normalize_pitch(float pitch) { + return (pitch+3.14) / (2*3.14); +} + +float normalize_roll(float roll) { + return (roll+0.28) / (2* 0.28); /*观测值*/ +} + +int main() { + + I2C_init(); + PCA9685_init(); + MPU6050_init(); + + /* 初始化相机 */ + //Camera_init(640, 480); + //Camera_check(); + //Camera_setVideoFormat(); + //Camera_reqBuf(); + //Camera_setup(); + //Camera_run(); + + + /* 创建归一化器 */ + //ImageLoader image_loader(3, 64, 64, true, false); + + // 创建网络组件 + //ConvLayer conv1(3, 8, 3, 1, 1); // 输入3通道,输出16通道,3x3卷积,padding=1 + //PoolingLayer pool1(2, 2, 2, 0, true); // 2x2最大池化,步长2 + //ConvLayer conv2(8, 16, 3, 1, 1); // 输入3通道,输出16通道,3x3卷积,padding=1 + //PoolingLayer pool2(2, 2, 2, 0, true); // 2x2最大池化,步长2 + //ConvLayer conv3(16, 32, 3, 1, 1); // 输入16通道,输出32通道 + //PoolingLayer pool3(2, 2, 2, 0, true); // 2x2最大池化,步长2 + //ConvLayer conv4(32, 64, 3, 1, 1); // 输入16通道,输出32通道 + //PoolingLayer pool4(2, 2, 2, 0, true); // 2x2最大池化,步长2 + //ConvLayer conv5(64, 128, 3, 1, 1); // 输入16通道,输出32通道 + //PoolingLayer pool5(2, 2, 2, 0, true); // 2x2最大池化,步长2 + //ConvLayer conv6(128, 256, 3, 1, 1); // 输入16通道,输出32通道 + //PoolingLayer pool6(2, 2, 2, 0, true); // 2x2最大池化,步长2 + //FlattenLayer flatten; // 展平层 + + // 使用CNN构建器 + //CNNBuilder cnn; + + // 定义输入尺寸 + //int input_channels = 3; + //int input_height = 64; + //int input_width = 64; + //int batch_size = 1; + + // 构建网络 + //cnn.add_conv_layer(conv1, input_channels, input_height, input_width); + //cnn.add_pool_layer(pool1, 8, input_height, input_width); // 卷积后尺寸不变(因padding=1) + //cnn.add_conv_layer(conv2, 8, input_height/2, input_width/2); // 池化后尺寸减半 + //cnn.add_pool_layer(pool2, 16, input_height/2, input_width/2); + //cnn.add_conv_layer(conv3, 16, input_height/4, input_width/4); // 池化后尺寸减半 + //cnn.add_pool_layer(pool3, 32, input_height/4, input_width/4); + //cnn.add_conv_layer(conv4, 32, input_height/8, input_width/8); // 池化后尺寸减半 + //cnn.add_pool_layer(pool4, 64, input_height/8, input_width/8); + //cnn.add_conv_layer(conv5, 64, input_height/16, input_width/16); // 池化后尺寸减半 + //cnn.add_pool_layer(pool5, 128, input_height/16, input_width/16); + //cnn.add_conv_layer(conv6, 128, input_height/32, input_width/32); // 池化后尺寸减半 + //cnn.add_pool_layer(pool6, 256, input_height/32, input_width/32); + //cnn.add_flatten_layer(flatten, 256, input_height/64, input_width/64); + + // 打印网络结构 + //cnn.print_architecture(); + + qing::NNBuilder nn; + using nnl = NeuralNetwork; + //auto layer1 = nnl::Create_in_Factory(256, 512, 0.01, nnl::ActivationFunc::Sigmoid); + //auto layer2 = nnl::Create_in_Factory(512, 256, 0.01, nnl::ActivationFunc::Sigmoid); + //auto layer3 = nnl::Create_in_Factory(256, 100, 0.01, nnl::ActivationFunc::Sigmoid); + //auto layer4 = nnl::Create_in_Factory(100, 10, 0.01, nnl::ActivationFunc::Sigmoid); + //nn.add(layer1); + //nn.add(layer2); + //nn.add(layer3); + //nn.add(layer4); + + auto layer1 = nnl::Create_in_Factory(18, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU); + auto layer2 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU); + auto layer3 = nnl::Create_in_Factory(36, 36, 0.01, nnl::ActivationFunc::Leaky_ReLU); + auto layer4 = nnl::Create_in_Factory(36, 16, 0.01, nnl::ActivationFunc::Sigmoid); + + nn.add(layer1); + nn.add(layer2); + nn.add(layer3); + nn.add(layer4); + nn.print_shape(); + + + auto parser = qing::ActParser(); + parser.fromStdin(); + auto bot = parser.get(); + while (true) { + try{ + std::cout << "-----------------------" << std::endl; + MPU6050_SensorData data = MPU6050_readSensorData(); + float pitch, roll; + MPU6050_calAngle(data, pitch, roll); + printf("pitch: %.2f\troll: %.2f\n"); + + /* 获取一个动作 */ + auto act = parser.now(); + auto x = std::vector(); + auto nor_pitch = normalize_pitch(pitch); + auto nor_roll = normalize_roll(roll); + x.push_back(nor_pitch); + x.push_back(nor_roll); + auto i{0}; + for (; i < act.size()-1; ++i) { + float a; + if (bot[i] == 0) { /* 空舵机位 */ + a = 0; + } + else { + a = (float)act[i] / bot[i]; + } + x.push_back(a); + std::cout << a << ","; + } + std::cout << std::endl; + + /* 前向反馈 */ + auto x1 = x; + auto o = nn.forward(x1); + //auto count1{0} + //for (auto& item: o) { + // x[count1] += x[count1] * o * 2 * 0.1; /* 10% */ + // count++; + //} + auto j{2}; + auto new_act = std::vector(); + for (; j (); + //err.push_back(); + //nor_pitch > 0) /* 计算误差 */ + // ? ((pitch1 > 0) ? nor_pitch - pitch1 : nor_pitch + pitch1 ) + // : ((pitch1 < 0) ? pitch1 - nor_pitch : -(nor_pitch + pitch1)); + + auto count3{0}; + for (auto& item: o) { + item *= err[count3++]; + } + + nn.backward(err); + + } + + catch(std::runtime_error& exp) + { + std::cout << exp.what() << std::endl; + break; + } + } + + //bool flag = true; + //while (flag) { + + //auto img = get_frame_from_camera(flag); + //auto image_data = image_loader.load_from_cimg(img, true); + //if (image_data.empty()) { + // std::cerr << "Failed to load image!" << std::endl; + // break; + //} + + /* 前向传播 */ + //auto output = cnn.forward(image_data, batch_size, true); + //if (!output.empty()) { + // std::cout << "Forward pass successful!" << std::endl; + // std::cout << "Input size: " << image_data.size() << std::endl; + // std::cout << "Output size: " << output.size() << std::endl; + + // 计算输出统计信息 + // float sum = 0.0f, max_val = output[0], min_val = output[0]; + // for (float val : output) { + // sum += val; + // if (val > max_val) max_val = val; + // if (val < min_val) min_val = val; + // } + + // std::cout << "Output statistics:" << std::endl; + // std::cout << " Mean: " << sum / output.size() << std::endl; + // std::cout << " Min: " << min_val << std::endl; + // std::cout << " Max: " << max_val << std::endl; + //} + + //std::vector t(16); + //auto r = nn.forward(output); + //auto y = nn.cal_err(r, t); + + //for (auto i: y) { + // std::cout << i << " "; + //} + //std::cout << std::endl; + + //y = nn.backward(y); + //y = cnn.backward(y); + + //std::this_thread::sleep_for(std::chrono::microseconds(10)); + + + //} + + //Camera_stop(); + //Camera_unsetup(); + return 0; +} diff --git a/lci/src/net/HttpServer.cpp b/lci/src/net/HttpServer.cpp new file mode 100644 index 0000000..d6fd7ee --- /dev/null +++ b/lci/src/net/HttpServer.cpp @@ -0,0 +1,417 @@ +#include "net/HttpServer.hpp" +#include +#include +#include + +namespace pmc { +namespace net { + +// Session类 - 处理单个HTTP连接 +class HttpServer::Session : public std::enable_shared_from_this { +public: + Session(tcp::socket&& socket, HttpServer* server) + : socket_(std::move(socket)), server_(server) { + } + + void run() { + doRead(); + } + +private: + void doRead() { + auto self = shared_from_this(); + http::async_read(socket_, buffer_, request_, + [this, self](beast::error_code ec, std::size_t) { + if (!ec) { + processRequest(); + } else if (ec != http::error::end_of_stream) { + // 错误处理 + } + }); + } + + void processRequest() { + try { + // 创建响应对象 + auto response = std::make_shared>( + server_->handleRequest(request_) + ); + doWrite(response); + } catch (const std::exception& e) { + std::cerr << "Error processing request: " << e.what() << std::endl; + // 发送错误响应 + auto error_response = std::make_shared>(); + error_response->version(request_.version()); + error_response->result(http::status::internal_server_error); + error_response->set(http::field::content_type, "text/plain"); + error_response->body() = "500 Internal Server Error\n"; + error_response->prepare_payload(); + doWrite(error_response); + } + } + + void doWrite(std::shared_ptr> response) { + auto self = shared_from_this(); + http::async_write(socket_, *response, + [this, self, response](beast::error_code ec, std::size_t) { + if (!ec) { + // 检查是否需要保持连接 + if (!response->keep_alive()) { + socket_.shutdown(tcp::socket::shutdown_send, ec); + } + } + // 继续读取下一个请求 + doRead(); + }); + } + + tcp::socket socket_; + beast::flat_buffer buffer_; + http::request request_; + HttpServer* server_; +}; + +// Listener类 - 接受新连接 +class HttpServer::Listener : public std::enable_shared_from_this { +public: + Listener(asio::io_context& ioc, tcp::endpoint endpoint, HttpServer* server) + : ioc_(ioc), acceptor_(ioc), server_(server) { + beast::error_code ec; + acceptor_.open(endpoint.protocol(), ec); + if (!ec) { + acceptor_.set_option(asio::socket_base::reuse_address(true), ec); + acceptor_.bind(endpoint, ec); + if (!ec) { + acceptor_.listen(asio::socket_base::max_listen_connections, ec); + } + } + if (ec) { + std::cerr << "Listener error: " << ec.message() << std::endl; + } + } + + void run() { + if (acceptor_.is_open()) { + doAccept(); + } + } + +private: + void doAccept() { + acceptor_.async_accept( + [this, self = shared_from_this()](beast::error_code ec, tcp::socket socket) { + if (!ec) { + std::make_shared(std::move(socket), server_)->run(); + } + doAccept(); + }); + } + + asio::io_context& ioc_; + tcp::acceptor acceptor_; + HttpServer* server_; +}; + +// HttpServer实现 - 新构造函数(监听所有接口) +HttpServer::HttpServer(unsigned short port, unsigned int threads) + : address_("0.0.0.0"), port_(port), threads_(threads), + ioc_(std::make_unique(threads)) { + if (!ioc_) { + throw std::runtime_error("Failed to create io_context"); + } +} + +// HttpServer实现 - 新构造函数(指定监听地址) +HttpServer::HttpServer(const std::string& address, unsigned short port, unsigned int threads) + : address_(address), port_(port), threads_(threads), + ioc_(std::make_unique(threads)) { + if (!ioc_) { + throw std::runtime_error("Failed to create io_context"); + } +} + +HttpServer::~HttpServer() { + stop(); +} + +void HttpServer::start() { + if (running_) { + return; + } + + try { + // 创建端点(使用指定的地址和端口) + asio::ip::address ip_address = asio::ip::make_address(address_); + tcp::endpoint endpoint(ip_address, port_); + + // 创建监听器 + listener_ = std::make_shared(*ioc_, endpoint, this); + if (!listener_) { + throw std::runtime_error("Failed to create listener"); + } + listener_->run(); + + running_ = true; + + // 启动工作线程 + for (unsigned int i = 0; i < threads_; ++i) { + worker_threads_.emplace_back([this]() { + try { + ioc_->run(); + } catch (const std::exception& e) { + std::cerr << "Thread exception: " << e.what() << std::endl; + } + }); + } + + std::cout << "HTTP Server started on " << address_ << ":" << port_ + << " with " << threads_ << " threads" << std::endl; + } catch (const std::exception& e) { + std::cerr << "Failed to start server: " << e.what() << std::endl; + running_ = false; + } +} + +void HttpServer::stop() { + if (!running_) { + return; + } + + running_ = false; + + if (ioc_) { + ioc_->stop(); + } + + for (auto& thread : worker_threads_) { + if (thread.joinable()) { + thread.join(); + } + } + worker_threads_.clear(); + + std::cout << "HTTP Server stopped" << std::endl; +} + +void HttpServer::get(const std::string& path, HttpRequestHandler handler) { + get_handlers_[path] = handler; +} + +void HttpServer::post(const std::string& path, HttpRequestHandler handler) { + post_handlers_[path] = handler; +} + +void HttpServer::put(const std::string& path, HttpRequestHandler handler) { + put_handlers_[path] = handler; +} + +void HttpServer::del(const std::string& path, HttpRequestHandler handler) { + delete_handlers_[path] = handler; +} + +void HttpServer::use(MiddlewareHandler middleware) { + middlewares_.push_back(middleware); +} + +void HttpServer::setStaticDirectory(const std::string& path) { + static_directory_ = path; +} + +bool HttpServer::isRunning() const { + return running_; +} + +unsigned short HttpServer::getPort() const { + return port_; +} + +std::string HttpServer::getAddress() const { + return address_; +} + +void HttpServer::run() { + start(); + if (ioc_) { + ioc_->run(); + } +} + +void HttpServer::showStatus() const { + std::cout << "=== HTTP Server Status ===" << std::endl; + std::cout << "Address: " << address_ << std::endl; + std::cout << "Port: " << port_ << std::endl; + std::cout << "Threads: " << threads_ << std::endl; + std::cout << "Running: " << (running_ ? "Yes" : "No") << std::endl; + std::cout << "Static Directory: " << (static_directory_.empty() ? "Not set" : static_directory_) << std::endl; + std::cout << "Registered Routes:" << std::endl; + std::cout << " GET: " << get_handlers_.size() << " handler(s)" << std::endl; + for (const auto& [path, _] : get_handlers_) { + std::cout << " GET " << path << std::endl; + } + std::cout << " POST: " << post_handlers_.size() << " handler(s)" << std::endl; + for (const auto& [path, _] : post_handlers_) { + std::cout << " POST " << path << std::endl; + } + std::cout << " PUT: " << put_handlers_.size() << " handler(s)" << std::endl; + for (const auto& [path, _] : put_handlers_) { + std::cout << " PUT " << path << std::endl; + } + std::cout << " DELETE: " << delete_handlers_.size() << " handler(s)" << std::endl; + for (const auto& [path, _] : delete_handlers_) { + std::cout << " DELETE " << path << std::endl; + } + std::cout << "Middlewares: " << middlewares_.size() << std::endl; + std::cout << "==========================" << std::endl; +} + +std::unordered_map HttpServer::parseQueryParams(const std::string& query) { + std::unordered_map params; + + if (query.empty()) { + return params; + } + + std::vector pairs; + boost::split(pairs, query, boost::is_any_of("&")); + + for (const auto& pair : pairs) { + std::vector key_value; + boost::split(key_value, pair, boost::is_any_of("=")); + + if (key_value.size() == 2) { + params[key_value[0]] = key_value[1]; + } else if (key_value.size() == 1) { + params[key_value[0]] = ""; + } + } + + return params; +} + +http::response HttpServer::handleRequest(const http::request& req) { + // 将 target() 转换为字符串 + std::string target_path = std::string(req.target().data(), req.target().size()); + std::string query_string; + + // 分离路径和查询参数 + auto query_pos = target_path.find('?'); + if (query_pos != std::string::npos) { + query_string = target_path.substr(query_pos + 1); + target_path = target_path.substr(0, query_pos); + } + + // 解析查询参数 + auto query_params = parseQueryParams(query_string); + + // 准备响应对象 + http::response response; + response.version(req.version()); + response.set(http::field::server, "Boost.Beast HttpServer"); + response.set(http::field::content_type, "text/plain"); + response.keep_alive(req.keep_alive()); + + // 执行中间件链 + bool middleware_handled = false; + for (const auto& middleware : middlewares_) { + if (!middleware(req, response, query_params)) { + middleware_handled = true; + break; + } + } + + // 如果中间件处理了请求,直接返回 + if (middleware_handled) { + response.prepare_payload(); + return response; + } + + // 查找路由处理器 + HttpRequestHandler handler = nullptr; + + switch (req.method()) { + case http::verb::get: { + auto it = get_handlers_.find(target_path); + if (it != get_handlers_.end()) { + handler = it->second; + } + break; + } + case http::verb::post: { + auto it = post_handlers_.find(target_path); + if (it != post_handlers_.end()) { + handler = it->second; + } + break; + } + case http::verb::put: { + auto it = put_handlers_.find(target_path); + if (it != put_handlers_.end()) { + handler = it->second; + } + break; + } + case http::verb::delete_: { + auto it = delete_handlers_.find(target_path); + if (it != delete_handlers_.end()) { + handler = it->second; + } + break; + } + default: + break; + } + + // 处理静态文件 + if (!handler && !static_directory_.empty()) { + std::string file_path = static_directory_ + target_path; + std::ifstream file(file_path, std::ios::binary); + + if (file.is_open()) { + std::stringstream buffer; + buffer << file.rdbuf(); + response.result(http::status::ok); + response.body() = buffer.str(); + + // 根据文件扩展名设置Content-Type + if (boost::algorithm::ends_with(file_path, ".html")) { + response.set(http::field::content_type, "text/html"); + } else if (boost::algorithm::ends_with(file_path, ".css")) { + response.set(http::field::content_type, "text/css"); + } else if (boost::algorithm::ends_with(file_path, ".js")) { + response.set(http::field::content_type, "application/javascript"); + } else if (boost::algorithm::ends_with(file_path, ".json")) { + response.set(http::field::content_type, "application/json"); + } else if (boost::algorithm::ends_with(file_path, ".png")) { + response.set(http::field::content_type, "image/png"); + } else if (boost::algorithm::ends_with(file_path, ".jpg") || + boost::algorithm::ends_with(file_path, ".jpeg")) { + response.set(http::field::content_type, "image/jpeg"); + } + + response.prepare_payload(); + return response; + } + } + + // 如果找到处理器,调用它 + if (handler) { + try { + return handler(req, query_params); + } catch (const std::exception& e) { + std::cerr << "Handler exception: " << e.what() << std::endl; + response.result(http::status::internal_server_error); + response.body() = "500 Internal Server Error\n"; + response.prepare_payload(); + return response; + } + } + + // 404 Not Found + response.result(http::status::not_found); + response.body() = "404 Not Found\n"; + response.prepare_payload(); + return response; +} + +} // namespace net +} // namespace pmc \ No newline at end of file diff --git a/lci/src/net/WebSocketClient.cpp b/lci/src/net/WebSocketClient.cpp new file mode 100644 index 0000000..0781ae9 --- /dev/null +++ b/lci/src/net/WebSocketClient.cpp @@ -0,0 +1,386 @@ +#include "net/WebSocketClient.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pmc { +namespace net { + +namespace beast = boost::beast; +namespace asio = boost::asio; +using tcp = asio::ip::tcp; + +// 内部实现结构体 +struct WebSocketClient::Impl { + // 配置参数 + std::string host; + std::string port; + std::string path; + + // Boost 对象 + asio::io_context ioc; + tcp::resolver resolver; + beast::websocket::stream ws; + + // 线程管理 + std::thread io_thread; + std::atomic running{false}; + + // 回调函数 + WebSocketClientBase::MessageCallback message_callback; + WebSocketClientBase::ErrorCallback error_callback; + WebSocketClientBase::ConnectCallback connect_callback; + WebSocketClientBase::CloseCallback close_callback; + + // 消息队列 + std::queue send_queue; + std::mutex queue_mutex; + std::condition_variable queue_cv; + std::atomic sending{false}; + + // 状态标志 + std::atomic connected{false}; + std::atomic connecting{false}; + std::atomic stopping{false}; + + // 配置选项 + bool auto_reconnect{false}; + int reconnect_interval_ms{3000}; + int heartbeat_interval_ms{30000}; + + // 构造函数 + Impl(const std::string& host, const std::string& port, const std::string& path) + : host(host) + , port(port) + , path(path) + , resolver(asio::make_strand(ioc)) + , ws(asio::make_strand(ioc)) + { + } + + // 析构函数 + ~Impl() { + stop(); + } + + // 启动IO线程 + void start() { + if (!running) { + running = true; + io_thread = std::thread([this]() { + while (running) { + try { + ioc.run(); + break; + } catch (const std::exception& e) { + std::cerr << "IO上下文异常: " << e.what() << std::endl; + } + } + }); + } + } + + // 停止IO线程 + void stop() { + running = false; + stopping = true; + + if (io_thread.joinable()) { + ioc.stop(); + io_thread.join(); + } + } + + // 执行连接 + void doConnect() { + if (connecting || connected) { + return; + } + + connecting = true; + connected = false; + + // 解析主机名 + resolver.async_resolve(host, port, + [this](beast::error_code ec, tcp::resolver::results_type results) { + onResolve(ec, results); + }); + } + + // 解析完成回调 + void onResolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) { + onError("解析失败: " + ec.message()); + return; + } + + // 连接到解析出的端点 + beast::get_lowest_layer(ws).async_connect(results, + [this](beast::error_code ec, tcp::resolver::results_type::endpoint_type) { + onConnect(ec); + }); + } + + // 连接完成回调 + void onConnect(beast::error_code ec) { + if (ec) { + onError("连接失败: " + ec.message()); + return; + } + + // 设置WebSocket选项 + ws.set_option(beast::websocket::stream_base::timeout::suggested( + beast::role_type::client)); + + // 执行WebSocket握手 + ws.async_handshake(host, path, + [this](beast::error_code ec) { + onHandshake(ec); + }); + } + + // 握手完成回调 + void onHandshake(beast::error_code ec) { + connecting = false; + + if (ec) { + onError("握手失败: " + ec.message()); + return; + } + + connected = true; + + // 调用连接成功回调 + if (connect_callback) { + connect_callback(true); + } + + // 开始读取消息 + doRead(); + + // 开始发送队列中的消息 + doSend(); + } + + // 读取消息 + void doRead() { + if (!connected) { + return; + } + + ws.async_read(buffer, + [this](beast::error_code ec, std::size_t bytes_transferred) { + onRead(ec, bytes_transferred); + }); + } + + // 读取完成回调 + void onRead(beast::error_code ec, std::size_t bytes_transferred) { + if (ec) { + if (ec == beast::websocket::error::closed) { + // 正常关闭 + onClose(); + } else { + onError("读取失败: " + ec.message()); + } + return; + } + + // 处理接收到的消息 + std::string message = beast::buffers_to_string(buffer.data()); + buffer.consume(bytes_transferred); + + if (message_callback) { + message_callback(message); + } + + // 继续读取 + doRead(); + } + + // 发送消息 + void doSend() { + if (!connected || sending) { + return; + } + + std::unique_lock lock(queue_mutex); + if (send_queue.empty()) { + return; + } + + sending = true; + std::string message = std::move(send_queue.front()); + send_queue.pop(); + lock.unlock(); + + ws.async_write(asio::buffer(message), + [this](beast::error_code ec, std::size_t bytes_transferred) { + onWrite(ec, bytes_transferred); + }); + } + + // 写入完成回调 + void onWrite(beast::error_code ec, std::size_t bytes_transferred) { + sending = false; + + if (ec) { + onError("写入失败: " + ec.message()); + return; + } + + // 继续发送队列中的消息 + doSend(); + } + + // 错误处理 + void onError(const std::string& error) { + connecting = false; + connected = false; + + std::cerr << "WebSocket错误: " << error << std::endl; + + if (error_callback) { + error_callback(error); + } + + // 自动重连 + if (auto_reconnect && !stopping) { + std::this_thread::sleep_for(std::chrono::milliseconds(reconnect_interval_ms)); + asio::post(ioc, [this]() { doConnect(); }); + } + } + + // 连接关闭 + void onClose() { + connected = false; + + if (close_callback) { + close_callback(); + } + + // 自动重连 + if (auto_reconnect && !stopping) { + std::this_thread::sleep_for(std::chrono::milliseconds(reconnect_interval_ms)); + asio::post(ioc, [this]() { doConnect(); }); + } + } + +private: + beast::flat_buffer buffer; +}; + +// WebSocketClient 实现 +WebSocketClient::WebSocketClient(const std::string& host, const std::string& port, const std::string& path) + : impl_(std::make_unique(host, port, path)) +{ + impl_->start(); +} + +WebSocketClient::~WebSocketClient() { + disconnect(); +} + +void WebSocketClient::setMessageCallback(MessageCallback callback) { + impl_->message_callback = callback; +} + +void WebSocketClient::setErrorCallback(ErrorCallback callback) { + impl_->error_callback = callback; +} + +void WebSocketClient::setConnectCallback(ConnectCallback callback) { + impl_->connect_callback = callback; +} + +void WebSocketClient::setCloseCallback(CloseCallback callback) { + impl_->close_callback = callback; +} + +bool WebSocketClient::connect(int timeout_ms) { + if (impl_->connecting || impl_->connected) { + return false; + } + + impl_->doConnect(); + return true; +} + +void WebSocketClient::disconnect() { + if (!impl_->connected && !impl_->connecting) { + return; + } + + impl_->stopping = true; + + asio::post(impl_->ioc, [this]() { + beast::error_code ec; + impl_->ws.close(beast::websocket::close_code::normal, ec); + impl_->connected = false; + impl_->connecting = false; + }); +} + +bool WebSocketClient::isConnected() const { + return impl_->connected; +} + +bool WebSocketClient::send(const std::string& message) { + if (!impl_->connected) { + return false; + } + + { + std::lock_guard lock(impl_->queue_mutex); + impl_->send_queue.push(message); + } + + impl_->queue_cv.notify_one(); + asio::post(impl_->ioc, [this]() { impl_->doSend(); }); + + return true; +} + +bool WebSocketClient::sendBinary(const void* data, size_t size) { + if (!impl_->connected) { + return false; + } + + std::string message(static_cast(data), size); + return send(message); +} + +void WebSocketClient::setAutoReconnect(bool enable, int interval_ms) { + impl_->auto_reconnect = enable; + impl_->reconnect_interval_ms = interval_ms; +} + +void WebSocketClient::setHeartbeatInterval(int interval_ms) { + impl_->heartbeat_interval_ms = interval_ms; +} + +std::string WebSocketClient::getHost() const { + return impl_->host; +} + +std::string WebSocketClient::getPort() const { + return impl_->port; +} + +std::string WebSocketClient::getPath() const { + return impl_->path; +} + +} // namespace net +} // namespace pmc \ No newline at end of file diff --git a/lci/src/net/WebSocketClientSSL.cpp b/lci/src/net/WebSocketClientSSL.cpp new file mode 100644 index 0000000..16d5f22 --- /dev/null +++ b/lci/src/net/WebSocketClientSSL.cpp @@ -0,0 +1,620 @@ +#include "net/WebSocketClientSSL.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pmc { +namespace net { + +namespace beast = boost::beast; +namespace asio = boost::asio; +namespace ssl = asio::ssl; +using tcp = asio::ip::tcp; +using websocket = beast::websocket::stream>; + +// 内部实现结构体 +struct WebSocketClientSSL::Impl { + // 配置参数 + std::string host; + std::string port; + std::string path; + + // SSL 配置 + bool verify_certificate{true}; + std::string cert_file; + std::string key_file; + std::string ca_file; + std::string ciphers; + std::string sni_hostname; + long ssl_options{ssl::context::default_workarounds | + ssl::context::no_sslv2 | + ssl::context::no_sslv3}; + + // Boost 对象 + asio::io_context ioc; + ssl::context ssl_ctx{ssl::context::tlsv12_client}; + tcp::resolver resolver; + websocket ws; + asio::steady_timer heartbeat_timer; + asio::steady_timer reconnect_timer; + + // 回调函数 + MessageCallback message_callback; + ErrorCallback error_callback; + ConnectCallback connect_callback; + CloseCallback close_callback; + + // 状态管理 + std::atomic connected{false}; + std::atomic connecting{false}; + std::atomic stopping{false}; + std::atomic auto_reconnect{false}; + std::atomic reconnect_interval{3000}; + std::atomic heartbeat_interval{30000}; + + // 消息队列 + struct QueuedMessage { + std::string data; + bool is_binary; + }; + std::queue send_queue; + std::mutex queue_mutex; + std::condition_variable queue_cv; + std::atomic writing{false}; + + // 接收缓冲区 + beast::flat_buffer read_buffer; + + // 线程 + std::thread io_thread; + + Impl(const std::string& h, const std::string& p, const std::string& pt) + : host(h) + , port(p) + , path(pt) + , resolver(asio::make_strand(ioc)) + , ws(asio::make_strand(ioc), ssl_ctx) + , heartbeat_timer(asio::make_strand(ioc)) + , reconnect_timer(asio::make_strand(ioc)) { + + // 配置 SSL 上下文 + configureSSLContext(); + } + + ~Impl() { + stopping = true; + if (io_thread.joinable()) { + ioc.stop(); + io_thread.join(); + } + } + + void configureSSLContext() { + try { + // 设置 SSL 选项 + ssl_ctx.set_options(ssl_options); + + // 设置密码列表 + if (!ciphers.empty()) { + SSL_CTX_set_cipher_list(ssl_ctx.native_handle(), ciphers.c_str()); + } + + // 加载证书文件 + if (!cert_file.empty()) { + ssl_ctx.use_certificate_file(cert_file, ssl::context::pem); + } + + // 加载私钥文件 + if (!key_file.empty()) { + ssl_ctx.use_private_key_file(key_file, ssl::context::pem); + } + + // 加载 CA 证书 + if (!ca_file.empty()) { + ssl_ctx.load_verify_file(ca_file); + } else { + // 使用系统默认的 CA 证书 + ssl_ctx.set_default_verify_paths(); + } + + // 设置验证模式 + if (verify_certificate) { + ssl_ctx.set_verify_mode(ssl::verify_peer | ssl::verify_fail_if_no_peer_cert); + } else { + ssl_ctx.set_verify_mode(ssl::verify_none); + } + + } catch (const std::exception& e) { + std::cerr << "SSL 上下文配置错误: " << e.what() << std::endl; + } + } + + void setSNI() { + // 设置 SNI 主机名 + if (!sni_hostname.empty()) { + if (!SSL_set_tlsext_host_name(ws.next_layer().native_handle(), sni_hostname.c_str())) { + beast::error_code ec{static_cast(::ERR_get_error()), asio::error::get_ssl_category()}; + throw beast::system_error{ec}; + } + } else if (!host.empty()) { + // 使用主机名作为 SNI + if (!SSL_set_tlsext_host_name(ws.next_layer().native_handle(), host.c_str())) { + beast::error_code ec{static_cast(::ERR_get_error()), asio::error::get_ssl_category()}; + throw beast::system_error{ec}; + } + } + } +}; + +// 构造函数 +WebSocketClientSSL::WebSocketClientSSL(const std::string& host, const std::string& port, const std::string& path) + : impl_(std::make_unique(host, port, path)) { +} + +// 析构函数 +WebSocketClientSSL::~WebSocketClientSSL() { + disconnect(); +} + +// 回调函数设置 +void WebSocketClientSSL::setMessageCallback(MessageCallback callback) { + impl_->message_callback = std::move(callback); +} + +void WebSocketClientSSL::setErrorCallback(ErrorCallback callback) { + impl_->error_callback = std::move(callback); +} + +void WebSocketClientSSL::setConnectCallback(ConnectCallback callback) { + impl_->connect_callback = std::move(callback); +} + +void WebSocketClientSSL::setCloseCallback(CloseCallback callback) { + impl_->close_callback = std::move(callback); +} + +// SSL/TLS 配置 +void WebSocketClientSSL::setVerifyCertificate(bool verify) { + impl_->verify_certificate = verify; + impl_->configureSSLContext(); +} + +void WebSocketClientSSL::setCertificateFile(const std::string& cert_file) { + impl_->cert_file = cert_file; + impl_->configureSSLContext(); +} + +void WebSocketClientSSL::setPrivateKeyFile(const std::string& key_file) { + impl_->key_file = key_file; + impl_->configureSSLContext(); +} + +void WebSocketClientSSL::setCertificateAuthorityFile(const std::string& ca_file) { + impl_->ca_file = ca_file; + impl_->configureSSLContext(); +} + +void WebSocketClientSSL::setSSLContextOptions(long options) { + impl_->ssl_options = options; + impl_->configureSSLContext(); +} + +void WebSocketClientSSL::setCiphers(const std::string& ciphers) { + impl_->ciphers = ciphers; + impl_->configureSSLContext(); +} + +void WebSocketClientSSL::setSNIHostname(const std::string& sni_hostname) { + impl_->sni_hostname = sni_hostname; +} + +// 连接函数 +bool WebSocketClientSSL::connect(int timeout_ms) { + if (impl_->connecting || impl_->connected) { + return false; + } + + impl_->connecting = true; + impl_->stopping = false; + + // 启动 IO 线程 + if (!impl_->io_thread.joinable()) { + impl_->io_thread = std::thread([this]() { + try { + impl_->ioc.run(); + } catch (const std::exception& e) { + if (impl_->error_callback) { + impl_->error_callback(std::string("IO context error: ") + e.what()); + } + } + }); + } + + // 异步连接 + asio::post(impl_->ioc, [this]() { + try { + impl_->setSNI(); + doConnect(); + } catch (const std::exception& e) { + handleError("SSL setup failed", beast::error_code{}, e.what()); + } + }); + + // 等待连接完成 + auto start = std::chrono::steady_clock::now(); + while (impl_->connecting && !impl_->stopping) { + if (timeout_ms > 0) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - start).count(); + if (elapsed > timeout_ms) { + impl_->connecting = false; + if (impl_->error_callback) { + impl_->error_callback("Connection timeout"); + } + return false; + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + return impl_->connected; +} + +// 私有连接方法 +void WebSocketClientSSL::doConnect() { + impl_->resolver.async_resolve( + impl_->host, + impl_->port, + [this](beast::error_code ec, tcp::resolver::results_type results) { + onResolve(ec, std::move(results)); + }); +} + +void WebSocketClientSSL::onResolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) { + handleError("Resolve failed", ec); + return; + } + + // 设置超时 + beast::get_lowest_layer(impl_->ws).expires_after(std::chrono::seconds(30)); + + // 异步连接 + beast::get_lowest_layer(impl_->ws).async_connect( + results, + [this](beast::error_code ec, tcp::resolver::results_type::endpoint_type endpoint) { + onConnect(ec, endpoint); + }); +} + +void WebSocketClientSSL::onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type endpoint) { + if (ec) { + handleError("Connect failed", ec); + return; + } + + // SSL 握手 + impl_->ws.next_layer().async_handshake( + ssl::stream_base::client, + [this](beast::error_code ec) { + onSSLHandshake(ec); + }); +} + +void WebSocketClientSSL::onSSLHandshake(beast::error_code ec) { + if (ec) { + handleError("SSL handshake failed", ec); + return; + } + + // 关闭 TCP 超时设置 + beast::get_lowest_layer(impl_->ws).expires_never(); + + // 设置 WebSocket 选项 + impl_->ws.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); + impl_->ws.read_message_max(10 * 1024 * 1024); // 10MB 限制 + + // WebSocket 握手 + impl_->ws.async_handshake( + impl_->host + ":" + impl_->port, + impl_->path, + [this](beast::error_code ec) { + onHandshake(ec); + }); +} + +void WebSocketClientSSL::onHandshake(beast::error_code ec) { + impl_->connecting = false; + + if (ec) { + handleError("WebSocket handshake failed", ec); + return; + } + + impl_->connected = true; + + // 开始读取消息 + doRead(); + + // 开始心跳 + if (impl_->heartbeat_interval > 0) { + startHeartbeat(); + } + + // 通知连接成功 + if (impl_->connect_callback) { + impl_->connect_callback(true); + } + + // 检查是否有待发送的消息 + if (hasMessages()) { + doWrite(); + } +} + +// 断开连接 +void WebSocketClientSSL::disconnect() { + impl_->stopping = true; + impl_->auto_reconnect = false; + + if (impl_->connected) { + asio::post(impl_->ioc, [this]() { + beast::error_code ec; + impl_->ws.close(websocket::close_code::normal, ec); + if (ec && impl_->error_callback) { + impl_->error_callback("Error closing WebSocket: " + ec.message()); + } + impl_->connected = false; + stopHeartbeat(); + + if (impl_->close_callback) { + impl_->close_callback(); + } + }); + } +} + +// 发送消息 +bool WebSocketClientSSL::send(const std::string& message) { + return pushMessage(message, false); +} + +bool WebSocketClientSSL::sendBinary(const void* data, size_t size) { + std::string message(static_cast(data), size); + return pushMessage(message, true); +} + +// 状态检查 +bool WebSocketClientSSL::isConnected() const { + return impl_->connected; +} + +std::string WebSocketClientSSL::getHost() const { + return impl_->host; +} + +std::string WebSocketClientSSL::getPort() const { + return impl_->port; +} + +std::string WebSocketClientSSL::getPath() const { + return impl_->path; +} + +// 自动重连和心跳设置 +void WebSocketClientSSL::setAutoReconnect(bool enable, int interval_ms) { + impl_->auto_reconnect = enable; + impl_->reconnect_interval = interval_ms; +} + +void WebSocketClientSSL::setHeartbeatInterval(int interval_ms) { + impl_->heartbeat_interval = interval_ms; + if (impl_->connected && interval_ms > 0) { + startHeartbeat(); + } +} + +// ============ 私有方法实现 ============ + +void WebSocketClientSSL::doRead() { + impl_->ws.async_read( + impl_->read_buffer, + [this](beast::error_code ec, std::size_t bytes_transferred) { + onRead(ec, bytes_transferred); + }); +} + +void WebSocketClientSSL::onRead(beast::error_code ec, std::size_t bytes_transferred) { + if (ec) { + if (ec == websocket::error::closed) { + // 正常关闭 + impl_->connected = false; + stopHeartbeat(); + + if (impl_->close_callback) { + impl_->close_callback(); + } + + // 自动重连 + if (impl_->auto_reconnect && !impl_->stopping) { + impl_->reconnect_timer.expires_after(std::chrono::milliseconds(impl_->reconnect_interval)); + impl_->reconnect_timer.async_wait( + [this](beast::error_code ec) { + if (!ec && !impl_->stopping) { + doConnect(); + } + }); + } + } else { + handleError("Read failed", ec); + } + return; + } + + // 处理接收到的消息 + if (impl_->message_callback) { + std::string message = beast::buffers_to_string(impl_->read_buffer.data()); + impl_->message_callback(message); + } + + // 清空缓冲区并继续读取 + impl_->read_buffer.consume(bytes_transferred); + doRead(); +} + +void WebSocketClientSSL::doWrite() { + if (impl_->writing || !hasMessages()) { + return; + } + + impl_->writing = true; + auto message = popMessage(); + + impl_->ws.async_write( + asio::buffer(message.first), + [this, is_binary = message.second](beast::error_code ec, std::size_t bytes_transferred) { + impl_->writing = false; + onWrite(ec, bytes_transferred); + + // 如果是二进制消息,设置二进制模式 + if (!ec && is_binary) { + impl_->ws.binary(true); + } else if (!ec) { + impl_->ws.text(true); + } + }); +} + +void WebSocketClientSSL::onWrite(beast::error_code ec, std::size_t bytes_transferred) { + (void)bytes_transferred; // 消除未使用参数警告 + + if (ec) { + handleError("Write failed", ec); + return; + } + + // 继续发送队列中的下一条消息 + if (hasMessages()) { + doWrite(); + } +} + +void WebSocketClientSSL::handleError(const std::string& message, beast::error_code ec, const std::string& extra) { + std::string error_msg = message; + if (ec) { + error_msg += ": " + ec.message(); + } + if (!extra.empty()) { + error_msg += " (" + extra + ")"; + } + + impl_->connecting = false; + impl_->connected = false; + stopHeartbeat(); + + if (impl_->error_callback) { + impl_->error_callback(error_msg); + } + + if (impl_->connect_callback) { + impl_->connect_callback(false); + } + + // 自动重连 + if (impl_->auto_reconnect && !impl_->stopping) { + impl_->reconnect_timer.expires_after(std::chrono::milliseconds(impl_->reconnect_interval)); + impl_->reconnect_timer.async_wait( + [this](beast::error_code ec) { + if (!ec && !impl_->stopping) { + doConnect(); + } + }); + } +} + +void WebSocketClientSSL::startHeartbeat() { + if (impl_->heartbeat_interval <= 0) { + return; + } + + impl_->heartbeat_timer.expires_after(std::chrono::milliseconds(impl_->heartbeat_interval)); + impl_->heartbeat_timer.async_wait( + [this](beast::error_code ec) { + heartbeatTimerCallback(ec); + }); +} + +void WebSocketClientSSL::stopHeartbeat() { + beast::error_code ec; + impl_->heartbeat_timer.cancel(ec); +} + +void WebSocketClientSSL::heartbeatTimerCallback(beast::error_code ec) { + if (ec || impl_->stopping || !impl_->connected) { + return; + } + + // 发送 ping 消息 + beast::error_code ping_ec; + impl_->ws.ping(beast::websocket::ping_data{}, ping_ec); + + if (ping_ec) { + handleError("Heartbeat ping failed", ping_ec); + return; + } + + // 重新设置定时器 + startHeartbeat(); +} + +// 线程安全队列操作 +bool WebSocketClientSSL::pushMessage(const std::string& message, bool is_binary) { + if (!impl_->connected || impl_->stopping) { + return false; + } + + { + std::lock_guard lock(impl_->queue_mutex); + impl_->send_queue.push({message, is_binary}); + } + + impl_->queue_cv.notify_one(); + + // 触发写入 + asio::post(impl_->ioc, [this]() { + if (!impl_->writing && hasMessages()) { + doWrite(); + } + }); + + return true; +} + +std::pair WebSocketClientSSL::popMessage() { + std::lock_guard lock(impl_->queue_mutex); + if (impl_->send_queue.empty()) { + return {"", false}; + } + + auto msg = std::move(impl_->send_queue.front()); + impl_->send_queue.pop(); + return {std::move(msg.data), msg.is_binary}; +} + +bool WebSocketClientSSL::hasMessages() const { + std::lock_guard lock(impl_->queue_mutex); + return !impl_->send_queue.empty(); +} + +} // namespace net +} // namespace pmc \ No newline at end of file diff --git a/lci/src/net/WebSocketServer.cpp b/lci/src/net/WebSocketServer.cpp new file mode 100644 index 0000000..78c4582 --- /dev/null +++ b/lci/src/net/WebSocketServer.cpp @@ -0,0 +1,382 @@ +// WebSocketServer.cpp - WebSocket服务器实现 +// 基于Boost.Beast和Boost.Asio + +#include "net/WebSocketServer.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace websocket = beast::websocket; +namespace asio = boost::asio; +using tcp = asio::ip::tcp; + +namespace pmc::net { + +// 内部Session类实现 +class WebSocketServer::Session : public std::enable_shared_from_this { +public: + Session(tcp::socket socket, WebSocketServer* server) + : ws_(std::move(socket)) + , server_(server) { + + // 获取远程地址 + auto endpoint = ws_.next_layer().socket().remote_endpoint(); + remoteAddress_ = endpoint.address().to_string() + ":" + std::to_string(endpoint.port()); + } + + void start() { + // 生成客户端ID + if (server_) { + clientId_ = server_->generateClientId(); + } else { + // 如果server为空,使用默认ID + static std::atomic counter{0}; + clientId_ = "client_" + std::to_string(++counter); + } + + // 异步接受WebSocket握手 + ws_.async_accept( + beast::bind_front_handler( + &Session::onAccept, + shared_from_this())); + } + + void send(const std::string& message) { + // 将消息添加到发送队列 + bool write_in_progress = !sendQueue_.empty(); + sendQueue_.push_back(message); + + if (!write_in_progress) { + // 开始异步写入 + doWrite(); + } + } + + const std::string& getClientId() const { return clientId_; } + const std::string& getRemoteAddress() const { return remoteAddress_; } + +private: + websocket::stream ws_; + beast::flat_buffer buffer_; + std::deque sendQueue_; + WebSocketServer* server_; + std::string clientId_; + std::string remoteAddress_; + + void onAccept(beast::error_code ec) { + if (ec) { + std::cerr << "WebSocket accept error: " << ec.message() << std::endl; + return; + } + + // 添加到服务器客户端列表 + if (server_) { + server_->addClient(clientId_, shared_from_this()); + + // 调用连接处理器 + if (server_->connectHandler_) { + server_->connectHandler_(clientId_, remoteAddress_); + } + } + + // 开始读取消息 + doRead(); + } + + void doRead() { + // 异步读取消息 + ws_.async_read( + buffer_, + beast::bind_front_handler( + &Session::onRead, + shared_from_this())); + } + + void onRead(beast::error_code ec, std::size_t bytes_transferred) { + if (ec == websocket::error::closed) { + // 正常关闭 + if (server_) { + server_->removeClient(clientId_); + if (server_->disconnectHandler_) { + server_->disconnectHandler_(clientId_); + } + } + return; + } + + if (ec) { + std::cerr << "WebSocket read error: " << ec.message() << std::endl; + if (server_) { + server_->removeClient(clientId_); + if (server_->disconnectHandler_) { + server_->disconnectHandler_(clientId_); + } + } + return; + } + + // 处理接收到的消息 + std::string message = beast::buffers_to_string(buffer_.data()); + buffer_.consume(bytes_transferred); + + if (server_) { + // 查找消息处理器 + for (const auto& handler : server_->messageHandlers_) { + // 这里可以添加路径匹配逻辑 + handler.second(clientId_, message, [this](const std::string& response) { + send(response); + }); + } + } + + // 继续读取 + doRead(); + } + + void doWrite() { + if (sendQueue_.empty()) { + return; + } + + // 获取队列中的第一条消息 + auto message = sendQueue_.front(); + + // 异步写入消息 + ws_.async_write( + asio::buffer(message), + beast::bind_front_handler( + &Session::onWrite, + shared_from_this())); + } + + void onWrite(beast::error_code ec, std::size_t bytes_transferred) { + if (ec) { + std::cerr << "WebSocket write error: " << ec.message() << std::endl; + if (server_) { + server_->removeClient(clientId_); + if (server_->disconnectHandler_) { + server_->disconnectHandler_(clientId_); + } + } + return; + } + + // 移除已发送的消息 + sendQueue_.pop_front(); + + // 继续发送队列中的下一条消息 + if (!sendQueue_.empty()) { + doWrite(); + } + } +}; + +// 内部Listener类实现 +class WebSocketServer::Listener : public std::enable_shared_from_this { +public: + static std::shared_ptr create(asio::io_context& ioc, tcp::endpoint endpoint, WebSocketServer* server) { + return std::shared_ptr(new Listener(ioc, endpoint, server)); + } + + void run() { + if (!acceptor_.is_open()) { + throw std::runtime_error("Acceptor is not open"); + } + + doAccept(); + } + +private: + Listener(asio::io_context& ioc, tcp::endpoint endpoint, WebSocketServer* server) + : ioc_(ioc) + , acceptor_(asio::make_strand(ioc)) + , server_(server) { + + beast::error_code ec; + + // 打开acceptor + acceptor_.open(endpoint.protocol(), ec); + if (ec) { + std::cerr << "Failed to open acceptor: " << ec.message() << std::endl; + throw std::runtime_error("Failed to open acceptor: " + ec.message()); + } + + // 设置地址重用 + acceptor_.set_option(asio::socket_base::reuse_address(true), ec); + if (ec) { + std::cerr << "Failed to set reuse address: " << ec.message() << std::endl; + throw std::runtime_error("Failed to set reuse address: " + ec.message()); + } + + // 绑定到端点 + acceptor_.bind(endpoint, ec); + if (ec) { + std::cerr << "Failed to bind to endpoint: " << ec.message() << std::endl; + throw std::runtime_error("Failed to bind to endpoint: " + ec.message()); + } + + // 开始监听 + acceptor_.listen(asio::socket_base::max_listen_connections, ec); + if (ec) { + std::cerr << "Failed to listen: " << ec.message() << std::endl; + throw std::runtime_error("Failed to listen: " + ec.message()); + } + } + + void doAccept() { + acceptor_.async_accept( + asio::make_strand(ioc_), + beast::bind_front_handler( + &Listener::onAccept, + shared_from_this())); + } + + void onAccept(beast::error_code ec, tcp::socket socket) { + if (ec) { + std::cerr << "Accept error: " << ec.message() << std::endl; + } else { + // 创建新的Session处理连接 + std::make_shared(std::move(socket), server_)->start(); + } + + // 继续接受新的连接 + doAccept(); + } + + asio::io_context& ioc_; + tcp::acceptor acceptor_; + WebSocketServer* server_; +}; + +WebSocketServer::WebSocketServer(unsigned short port, unsigned int threads) + : port_(port) + , threads_(threads) + , impl_(std::make_unique()) {} + +WebSocketServer::~WebSocketServer() { + stop(); +} + +void WebSocketServer::start() { + if (running_) { + return; + } + + // 创建监听器 + tcp::endpoint endpoint(tcp::v4(), port_); + impl_->listener_ = Listener::create(impl_->ioc_, endpoint, this); + + // 启动监听器 + impl_->listener_->run(); + + // 启动工作线程 + for (unsigned int i = 0; i < threads_; ++i) { + impl_->threads_.emplace_back([this]() { + impl_->ioc_.run(); + }); + } + + running_ = true; + std::cout << "WebSocketServer started on port " << port_ << " with " << threads_ << " threads" << std::endl; +} + +void WebSocketServer::stop() { + if (!running_) { + return; + } + + // 停止io_context + impl_->ioc_.stop(); + + // 等待所有线程结束 + for (auto& thread : impl_->threads_) { + if (thread.joinable()) { + thread.join(); + } + } + + impl_->threads_.clear(); + running_ = false; + std::cout << "WebSocketServer stopped" << std::endl; +} + +void WebSocketServer::onMessage(const std::string& path, WebSocketMessageHandler handler) { + messageHandlers_[path] = std::move(handler); +} + +void WebSocketServer::onConnect(WebSocketConnectHandler handler) { + connectHandler_ = std::move(handler); +} + +void WebSocketServer::onDisconnect(WebSocketDisconnectHandler handler) { + disconnectHandler_ = std::move(handler); +} + +bool WebSocketServer::sendToClient(const std::string& clientId, const std::string& message) { + std::lock_guard lock(clientsMutex_); + auto it = clients_.find(clientId); + if (it != clients_.end()) { + it->second->send(message); + return true; + } + return false; +} + +size_t WebSocketServer::broadcast(const std::string& message) { + std::lock_guard lock(clientsMutex_); + size_t count = 0; + for (auto& pair : clients_) { + pair.second->send(message); + count++; + } + return count; +} + +void WebSocketServer::disconnectClient(const std::string& clientId) { + std::lock_guard lock(clientsMutex_); + auto it = clients_.find(clientId); + if (it != clients_.end()) { + // 这里应该关闭WebSocket连接 + // 目前只是从列表中移除 + clients_.erase(it); + } +} + +size_t WebSocketServer::getClientCount() const { + std::lock_guard lock(clientsMutex_); + return clients_.size(); +} + +std::string WebSocketServer::generateClientId() const { + static std::atomic counter{0}; + return "client_" + std::to_string(++counter); +} + +void WebSocketServer::addClient(const std::string& clientId, std::shared_ptr session) { + std::lock_guard lock(clientsMutex_); + clients_[clientId] = session; +} + +void WebSocketServer::removeClient(const std::string& clientId) { + std::lock_guard lock(clientsMutex_); + clients_.erase(clientId); +} + +std::shared_ptr WebSocketServer::getClient(const std::string& clientId) { + std::lock_guard lock(clientsMutex_); + auto it = clients_.find(clientId); + if (it != clients_.end()) { + return it->second; + } + return nullptr; +} + +} // namespace pmc::net \ No newline at end of file diff --git a/lci/src/th/Thread.cpp b/lci/src/th/Thread.cpp new file mode 100644 index 0000000..35969d1 --- /dev/null +++ b/lci/src/th/Thread.cpp @@ -0,0 +1,48 @@ +#include "th/Thread.hpp" +namespace qing{ + + Thread::Thread(f_t stop_callback, f_t wake_callback, f_t loop_callback, f_t clear_callback) + { + StopEvent = std::make_unique(stop_callback); + WakeEvent = std::make_unique(wake_callback); + LoopEvent = std::make_unique(loop_callback); + ClearEvent = std::make_unique(clear_callback); + }; + + /* 线程主函数 */ + void Thread::main() { + + /* 回字形循环 */ + while (check() != Fsm::Stat::SHUT) { + + try { /* 异常只会使线程静止 */ + + + while (check() == Fsm::Stat::STOP) { + (*StopEvent)(*this); /* 静止状态 */ + } + + if (check() == Fsm::Stat::START) { + //(*ClearEvent)(*this); /* 在开始之前做一次清理 */ + (*WakeEvent)(*this); /* 开始事件 */ + } + + while (check() == Fsm::Stat::RUNNING) { + (*LoopEvent)(*this); /* 从设置阶段进入运行态 */ + } + + /* 清理事件 (记得检查成员变量有没有被设置) */ + (*ClearEvent)(*this); + + + } + + catch (std::exception& exp) { + std::cout << "Thread throw a exception: " << exp.what() << std::endl; + stop(); /* 因为这里的停止,线程在循环内抛出异常进程不会死亡,所以需要特别注意内存泄漏 */ + } + + } + } + +}