diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1bdd2d5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+info.log
+msm*
+templates
+vue-minecraft/node_modules
+data.db
+route/template
+.data*
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..dbbe355
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 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 Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are 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.
+
+ 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.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ 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 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 work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ 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 AGPL, see
+.
diff --git a/api/api.go b/api/api.go
new file mode 100644
index 0000000..96bb8a6
--- /dev/null
+++ b/api/api.go
@@ -0,0 +1,33 @@
+package api
+
+import (
+ "errors"
+ "msm/consts/ctxflag"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+func rOk(ctx *gin.Context, message string, data any) {
+ jsonData := map[string]any{
+ "code": 0,
+ "msg": message,
+ }
+ if data != nil {
+ jsonData["data"] = data
+ }
+ ctx.JSON(http.StatusOK, jsonData)
+}
+
+func errCheck(ctx *gin.Context, isErr bool, errData any) {
+ if !isErr {
+ return
+ }
+ if err, ok := errData.(error); ok {
+ ctx.Set(ctxflag.ERR, err)
+ }
+ if err, ok := errData.(string); ok {
+ ctx.Set(ctxflag.ERR, errors.New(err))
+ }
+ panic(0)
+}
diff --git a/api/config.go b/api/config.go
new file mode 100644
index 0000000..cdd4470
--- /dev/null
+++ b/api/config.go
@@ -0,0 +1,94 @@
+package api
+
+import (
+ "msm/config"
+ "msm/dao"
+ "msm/model"
+ "msm/service/es"
+ "reflect"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+type configApi struct{}
+
+var ConfigApi = new(configApi)
+
+func (c *configApi) GetSystemConfiguration(ctx *gin.Context) {
+ typeElem := reflect.TypeOf(config.CF).Elem()
+ valueElem := reflect.ValueOf(config.CF).Elem()
+ result := []model.SystemConfigurationResp{}
+ for i := 0; i < typeElem.NumField(); i++ {
+ typeField := typeElem.Field(i)
+ valueField := valueElem.Field(i)
+ var value any
+ switch typeField.Type.Kind() {
+ case reflect.Int64, reflect.Int:
+ value = valueField.Int()
+ case reflect.String:
+ value = valueField.String()
+ case reflect.Bool:
+ value = valueField.Bool()
+ case reflect.Float64:
+ value = valueField.Float()
+ default:
+ continue
+ }
+ result = append(result, model.SystemConfigurationResp{
+ Key: typeField.Name,
+ Value: value,
+ Default: typeField.Tag.Get("default"),
+ Describe: typeField.Tag.Get("describe"),
+ })
+ }
+ rOk(ctx, "获取系统配置成功", result)
+}
+
+func (c *configApi) SetSystemConfiguration(ctx *gin.Context) {
+ data := map[string]string{}
+ errCheck(ctx, ctx.ShouldBindJSON(&data) != nil, "请求参数错误")
+ typeElem := reflect.TypeOf(config.CF).Elem()
+ valueElem := reflect.ValueOf(config.CF).Elem()
+ for i := 0; i < typeElem.NumField(); i++ {
+ typeField := typeElem.Field(i)
+ valueField := valueElem.Field(i)
+ for k, v := range data {
+ if typeField.Name == k {
+ var err error
+ switch typeField.Type.Kind() {
+ case reflect.String:
+ valueField.SetString(v)
+ case reflect.Bool:
+ value, errV := strconv.ParseBool(v)
+ err = errV
+ if err == nil {
+ valueField.SetBool(value)
+ }
+ case reflect.Float64:
+ value, errV := strconv.ParseFloat(v, 64)
+ err = errV
+ if err == nil {
+ valueField.SetFloat(value)
+ }
+ case reflect.Int64, reflect.Int:
+ value, errV := strconv.ParseInt(v, 10, 64)
+ err = errV
+ if err == nil {
+ valueField.SetInt(value)
+ }
+ default:
+ continue
+ }
+ errCheck(ctx, err != nil, k+"类似错误")
+ errCheck(ctx, dao.ConfigDao.SetConfigValue(k, v) != nil, "修改配置失败")
+ }
+ }
+ }
+ rOk(ctx, "修改配置成功", nil)
+}
+
+func (c *configApi) EsConfigReload(ctx *gin.Context) {
+ errCheck(ctx, !es.InitEs(), "es连接失败,请检查是否启用es或账号密码是否存在错误")
+ rOk(ctx, "已连接上es", nil)
+}
diff --git a/api/file.go b/api/file.go
new file mode 100644
index 0000000..51f604a
--- /dev/null
+++ b/api/file.go
@@ -0,0 +1,34 @@
+package api
+
+import (
+ FileService "msm/service/file"
+
+ "github.com/gin-gonic/gin"
+)
+
+type file struct{}
+
+var FileApi = new(file)
+
+func (f *file) FilePathHandler(ctx *gin.Context) {
+ data, err := FileService.FileService.GetFileAndDirByPath(ctx.Query("path"))
+ errCheck(ctx, err != nil, "文件路径查询失败")
+ rOk(ctx, "文件路径查询成功", data)
+}
+
+func (f *file) FileWriteHandler(ctx *gin.Context) {
+ path := ctx.PostForm("filePath")
+ fi, err := ctx.FormFile("data")
+ errCheck(ctx, err != nil, "文件读取失败")
+ fiReader, _ := fi.Open()
+ err = FileService.FileService.UpdateFileData(path, fiReader, fi.Size)
+ errCheck(ctx, err != nil, "文件数据更新失败")
+ rOk(ctx, "文件更新成功", nil)
+}
+
+func (f *file) FileReadHandler(ctx *gin.Context) {
+ path := ctx.Query("filePath")
+ bytes, err := FileService.FileService.ReadFileFromPath(path)
+ errCheck(ctx, err != nil, "文件数据读取失败")
+ rOk(ctx, "文件数据读取成功", string(bytes))
+}
diff --git a/api/log.go b/api/log.go
new file mode 100644
index 0000000..3fd8ec5
--- /dev/null
+++ b/api/log.go
@@ -0,0 +1,20 @@
+package api
+
+import (
+ "msm/config"
+ "msm/model"
+ "msm/service/es"
+
+ "github.com/gin-gonic/gin"
+)
+
+type logApi struct{}
+
+var LogApi = new(logApi)
+
+func (a *logApi) GetLog(ctx *gin.Context) {
+ req := model.GetLogReq{}
+ errCheck(ctx, !config.CF.EsEnable, "elasticsearch未启用或账号密码错误")
+ errCheck(ctx, ctx.ShouldBindJSON(&req) != nil, "请求体格式错误")
+ rOk(ctx, "查询成功", es.EsService.Search(req))
+}
diff --git a/api/permission.go b/api/permission.go
new file mode 100644
index 0000000..af4bcf7
--- /dev/null
+++ b/api/permission.go
@@ -0,0 +1,27 @@
+package api
+
+import (
+ "msm/model"
+
+ "msm/dao"
+
+ "github.com/gin-gonic/gin"
+)
+
+var PermissionApi = new(permissionApi)
+
+type permissionApi struct{}
+
+func (p *permissionApi) EditPermssion(ctx *gin.Context) {
+ per := model.Permission{}
+ err := ctx.ShouldBindJSON(&per)
+ errCheck(ctx, err != nil, err)
+ err = dao.PermissionDao.EditPermssion(per)
+ errCheck(ctx, err != nil, err)
+ rOk(ctx, "权限修改成功", nil)
+}
+
+func (p *permissionApi) GetPermissionList(ctx *gin.Context) {
+ result := dao.PermissionDao.GetPermssionList(ctx.Query("account"))
+ rOk(ctx, "查询成功", result)
+}
diff --git a/api/proc.go b/api/proc.go
new file mode 100644
index 0000000..857801e
--- /dev/null
+++ b/api/proc.go
@@ -0,0 +1,101 @@
+package api
+
+import (
+ "msm/consts/ctxflag"
+ "msm/consts/role"
+ "msm/dao"
+ "msm/model"
+ "msm/service/process"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+type procApi struct{}
+
+var ProcApi = new(procApi)
+
+func (p *procApi) CreateNewProcess(ctx *gin.Context) {
+ req := model.Process{}
+ ctx.ShouldBindJSON(&req)
+ index, err := dao.ProcessDao.AddProcessConfig(req)
+ errCheck(ctx, err != nil, err)
+ req.Uuid = index
+ proc, err := process.RunNewProcess(req)
+ errCheck(ctx, err != nil, err)
+ process.ProcessCtlService.AddProcess(req.Uuid, proc)
+ rOk(ctx, "创建成功", gin.H{
+ "id": req.Uuid,
+ })
+}
+
+func (p *procApi) DeleteNewProcess(ctx *gin.Context) {
+ uuid, err := strconv.Atoi(ctx.Query("uuid"))
+ errCheck(ctx, err != nil, "参数有误")
+ process.ProcessCtlService.KillProcess(uuid)
+ process.ProcessCtlService.DeleteProcess(uuid)
+ err = dao.ProcessDao.DeleteProcessConfig(uuid)
+ errCheck(ctx, err != nil, err)
+ rOk(ctx, "删除成功", nil)
+}
+
+func (p *procApi) KillProcess(ctx *gin.Context) {
+ uuid, err := strconv.Atoi(ctx.Query("uuid"))
+ errCheck(ctx, err != nil, "参数有误")
+ err = process.ProcessCtlService.KillProcess(uuid)
+ errCheck(ctx, err != nil, err)
+ rOk(ctx, "成功", nil)
+}
+
+func (p *procApi) StartProcess(ctx *gin.Context) {
+ uuid, err := strconv.Atoi(ctx.Query("uuid"))
+ errCheck(ctx, err != nil, "参数有误")
+ prod, err := process.ProcessCtlService.GetProcess(uuid)
+ if err != nil { // 进程不存在则创建
+ proc, err := process.RunNewProcess(dao.ProcessDao.GetProcessConfigById(uuid))
+ errCheck(ctx, err != nil, err)
+ process.ProcessCtlService.AddProcess(uuid, proc)
+ rOk(ctx, "成功", nil)
+ return
+ }
+ errCheck(ctx, prod.GetStateState() == 1, "进程还在运行中")
+ prod.ResetRestartTimes()
+ prod.ReStart()
+ // dao.UpdateServerAutoStart(uuid, true)
+ rOk(ctx, "成功", nil)
+}
+
+func (p *procApi) GetProcessList(ctx *gin.Context) {
+ if ctx.GetInt(ctxflag.ROLE) < int(role.USER) {
+ rOk(ctx, "进程列表获取成功", process.ProcessCtlService.GetProcessList())
+ } else {
+ rOk(ctx, "进程列表获取成功", process.ProcessCtlService.GetProcessListByUser(ctx.GetString(ctxflag.USER_NAME)))
+ }
+}
+
+func (p *procApi) UpdateProcessConfig(ctx *gin.Context) {
+ req := model.Process{}
+ ctx.ShouldBindJSON(&req)
+ process.ProcessCtlService.UpdateProcessConfig(req)
+ err := dao.ProcessDao.UpdateProcessConfig(req)
+ errCheck(ctx, err != nil, err)
+ rOk(ctx, "更改配置成功", nil)
+}
+
+func (p *procApi) GetProcessConfig(ctx *gin.Context) {
+ uuid, err := strconv.Atoi(ctx.Query("uuid"))
+ errCheck(ctx, err != nil, "参数有误")
+ data := dao.ProcessDao.GetProcessConfigById(uuid)
+ errCheck(ctx, data.Uuid == 0, "未查询到信息")
+ rOk(ctx, "success", data)
+}
+
+func (p *procApi) ProcessControl(ctx *gin.Context) {
+ user := ctx.GetString(ctxflag.USER_NAME)
+ uuid, err := strconv.Atoi(ctx.Query("uuid"))
+ errCheck(ctx, err != nil, "参数有误")
+ proc, err := process.ProcessCtlService.GetProcess(uuid)
+ errCheck(ctx, err != nil, "进程控制权获取失败")
+ proc.ProcessControl(user)
+ rOk(ctx, "获取进程控权成功", nil)
+}
diff --git a/api/push.go b/api/push.go
new file mode 100644
index 0000000..595aff9
--- /dev/null
+++ b/api/push.go
@@ -0,0 +1,50 @@
+package api
+
+import (
+ "msm/model"
+ "strconv"
+
+ "msm/dao"
+
+ "github.com/gin-gonic/gin"
+)
+
+type pushApi struct{}
+
+var PushApi = new(pushApi)
+
+func (p *pushApi) GetPushList(ctx *gin.Context) {
+ rOk(ctx, "查询成功", dao.PushDao.GetPushList())
+}
+
+func (p *pushApi) GetPushById(ctx *gin.Context) {
+ id, err := strconv.Atoi(ctx.Query("id"))
+ errCheck(ctx, err != nil, err)
+ rOk(ctx, "查询成功", dao.PushDao.GetPushConfigById(id))
+}
+
+func (p *pushApi) AddPushConfig(ctx *gin.Context) {
+ data := model.Push{}
+ err := ctx.ShouldBindJSON(&data)
+ errCheck(ctx, err != nil, err)
+ err = dao.PushDao.AddPushConfig(data)
+ errCheck(ctx, err != nil, err)
+ rOk(ctx, "添加成功", nil)
+}
+
+func (p *pushApi) UpdatePushConfig(ctx *gin.Context) {
+ data := model.Push{}
+ err := ctx.ShouldBindJSON(&data)
+ errCheck(ctx, err != nil, err)
+ err = dao.PushDao.UpdatePushConfig(data)
+ errCheck(ctx, err != nil, err)
+ rOk(ctx, "更新成功", nil)
+}
+
+func (p *pushApi) DeletePushConfig(ctx *gin.Context) {
+ id, err := strconv.Atoi(ctx.Query("id"))
+ errCheck(ctx, err != nil, err)
+ err = dao.PushDao.DeletePushConfig(id)
+ errCheck(ctx, err != nil, err)
+ rOk(ctx, "删除成功", nil)
+}
diff --git a/api/user.go b/api/user.go
new file mode 100644
index 0000000..44e4dfd
--- /dev/null
+++ b/api/user.go
@@ -0,0 +1,86 @@
+package api
+
+import (
+ "msm/consts/ctxflag"
+ "msm/consts/role"
+ "msm/dao"
+ "msm/model"
+ "msm/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+type userApi struct{}
+
+var UserApi = new(userApi)
+
+const DEFAULT_ROOT_PASSWORD = "root"
+
+func (u *userApi) LoginHandler(ctx *gin.Context) {
+ info := map[string]string{}
+ ctx.ShouldBindJSON(&info)
+ account := info["account"]
+ password := info["password"]
+ errCheck(ctx, !u.checkLoginInfo(account, password), "登入失败,账号或密码错误")
+ token, err := utils.GenToken(account)
+ errCheck(ctx, err != nil, err)
+ ctx.JSON(200, gin.H{
+ "code": 0,
+ "msg": "登入成功!",
+ "token": token,
+ "username": account,
+ "role": dao.UserDao.GetUserByName(account).Role,
+ })
+}
+
+func (u *userApi) CreateUser(ctx *gin.Context) {
+ user := model.User{}
+ err := ctx.ShouldBindJSON(&user)
+ errCheck(ctx, err != nil, err)
+ errCheck(ctx, user.Role == int(role.ROOT), "不能添加root账号")
+ err = dao.UserDao.CreateUser(user)
+ errCheck(ctx, err != nil, err)
+ rOk(ctx, "注册成功", nil)
+}
+
+func (u *userApi) ChangePassword(ctx *gin.Context) {
+ user := model.User{}
+ err := ctx.ShouldBindJSON(&user)
+ errCheck(ctx, err != nil, err)
+ reqUser := ctx.GetString(ctxflag.USER_NAME)
+ errCheck(ctx, ctx.GetInt(ctxflag.ROLE) != int(role.ROOT) && user.Account != "", "参数错误")
+ var userName string
+ if user.Account != "" {
+ userName = user.Account
+ } else {
+ userName = reqUser
+ }
+ err = dao.UserDao.UpdatePassword(userName, user.Password)
+ errCheck(ctx, err != nil, err)
+ rOk(ctx, "修改密码成功", nil)
+
+}
+
+func (u *userApi) DeleteUser(ctx *gin.Context) {
+ errCheck(ctx, ctx.Query("account") == "root", "无法删除root账户")
+ err := dao.UserDao.DeleteUser(ctx.Query("account"))
+ errCheck(ctx, err != nil, "无法删除root账户")
+ rOk(ctx, "删除成功", nil)
+}
+
+func (u *userApi) GetUserList(ctx *gin.Context) {
+ rOk(ctx, "查询成功", dao.UserDao.GetUserList())
+}
+
+func (u *userApi) checkLoginInfo(account, password string) bool {
+ user := dao.UserDao.GetUserByName(account)
+ if account == "root" && user.Account == "" {
+ dao.UserDao.CreateUser(model.User{
+ Account: "root",
+ Password: DEFAULT_ROOT_PASSWORD,
+ Role: int(role.ROOT),
+ })
+ return password == DEFAULT_ROOT_PASSWORD
+ }
+ return user.Password == utils.Md5(password)
+}
diff --git a/api/ws.go b/api/ws.go
new file mode 100644
index 0000000..6416ae3
--- /dev/null
+++ b/api/ws.go
@@ -0,0 +1,96 @@
+package api
+
+import (
+ "context"
+ "msm/consts/ctxflag"
+ "msm/log"
+ "msm/service/process"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+)
+
+type wsApi struct{}
+
+var WsApi = new(wsApi)
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ CheckOrigin: func(r *http.Request) bool {
+ return true
+ },
+}
+
+func (w *wsApi) WebsocketHandle(ctx *gin.Context) {
+ reqUser := ctx.GetString(ctxflag.USER_NAME)
+ uuid, err := strconv.Atoi(ctx.Query("uuid"))
+ errCheck(ctx, err != nil, "参数有误")
+ proc, err := process.ProcessCtlService.GetProcess(uuid)
+ errCheck(ctx, err != nil, "进程获取失败")
+ errCheck(ctx, proc.GetStateState() != 1, "进程未运行")
+ errCheck(ctx, proc.GetControlController() != reqUser && !proc.VerifyControl(), "进程权限不足")
+ errCheck(ctx, !proc.TryLock(), "进程已被占用")
+ proc.SetWhoUsing(reqUser)
+ conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
+ errCheck(ctx, err != nil, "ws升级失败")
+ log.Logger.Infow("ws连接成功", "进程名称", proc.GetName(), "连接者", proc.GetWhoUsing())
+ proc.SetControlController("")
+ wsCtx, cancel := context.WithCancel(context.Background())
+ w.startWsConnect(conn, proc, cancel)
+ proc.SetWsConn(conn)
+ proc.SetIsUsing(true)
+ close := func(err string) {
+ proc.SetWhoUsing("")
+ proc.SetIsUsing(false)
+ proc.SetWsConn(nil)
+ conn.Close()
+ proc.Unlock()
+ log.Logger.Infow("ws连接断开", "操作类型", err, "进程名称", proc.GetName())
+ }
+ conn.SetCloseHandler(func(_ int, _ string) error {
+ proc.ChangControlChan() <- 1
+ close("ws连接被断开")
+ return nil
+ })
+ select {
+ case signal := <-proc.ChangControlChan():
+ {
+ if signal == 0 {
+ close("强制断开ws连接")
+ }
+ }
+ case <-proc.StopChan():
+ {
+ close("进程已停止,强制断开ws连接")
+ }
+ case <-time.After(time.Minute * 10):
+ {
+ close("连接时间超过最大时长限制")
+ }
+ case <-wsCtx.Done():
+ {
+ close("tcp连接建立已被关闭")
+ }
+ }
+
+}
+
+func (w *wsApi) startWsConnect(conn *websocket.Conn, proc process.Process, cancel context.CancelFunc) {
+ proc.ReadCache(conn)
+ log.Logger.Debugw("ws读取线程已启动")
+ go func() {
+ for {
+ _, b, err := conn.ReadMessage()
+ if err != nil {
+ log.Logger.Debugw("ws读取线程已退出", "info", err)
+ cancel()
+ return
+ }
+ proc.WriteBytes(b)
+ }
+ }()
+}
diff --git a/boot/boot.go b/boot/boot.go
new file mode 100644
index 0000000..fec4354
--- /dev/null
+++ b/boot/boot.go
@@ -0,0 +1,52 @@
+package boot
+
+import (
+ "msm/config"
+ "msm/dao"
+ "msm/log"
+ "msm/service/es"
+ "msm/service/process"
+ "msm/utils"
+ "reflect"
+ "strconv"
+)
+
+func Boot() {
+ initConfiguration()
+ initEs()
+ initProcess()
+}
+
+func initConfiguration() {
+ typeElem := reflect.TypeOf(config.CF).Elem()
+ valueElem := reflect.ValueOf(config.CF).Elem()
+ for i := 0; i < typeElem.NumField(); i++ {
+ typeField := typeElem.Field(i)
+ valueField := valueElem.Field(i)
+ value, err := dao.ConfigDao.GetConfigValue(typeField.Name)
+ if err != nil {
+ value = typeField.Tag.Get("default")
+ }
+ switch typeField.Type.Kind() {
+ case reflect.String:
+ valueField.SetString(value)
+ case reflect.Bool:
+ valueField.SetBool(utils.Unwarp(strconv.ParseBool(value)))
+ case reflect.Float64:
+ valueField.SetFloat(utils.Unwarp(strconv.ParseFloat(value, 64)))
+ case reflect.Int64, reflect.Int:
+ valueField.SetInt(utils.Unwarp(strconv.ParseInt(value, 10, 64)))
+ default:
+ continue
+ }
+ }
+ log.Logger.Debugw("获取配置信息完成", "Configuration", config.CF)
+}
+
+func initEs() {
+ es.InitEs()
+}
+
+func initProcess() {
+ process.ProcessCtlService.ProcessInit()
+}
diff --git a/build.bat b/build.bat
new file mode 100644
index 0000000..ede841d
--- /dev/null
+++ b/build.bat
@@ -0,0 +1,4 @@
+ SET CGO_ENABLED=0
+ SET GOOS=linux
+ SET GOARCH=amd64
+ go build -o msm main.go
\ No newline at end of file
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..96529ba
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,8 @@
+LogLevel: "debug"
+Listen: ":8797"
+EsConfig:
+ Enable: true
+ Url: "http://xcon.top:9200"
+ Index: "server_log_v1"
+ Username: "elastic"
+ Password: "1625167628@xcon"
\ No newline at end of file
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..da9d5c2
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,27 @@
+package config
+
+var CF = &configuration{
+ LogLevel: "debug",
+}
+
+// 只支持 float64、int、int64、bool、string类型
+type configuration struct {
+ LogLevel string `default:"debug" describe:"日志等级"`
+ Listen string `default:":8797" describe:"监听端口"`
+ EsEnable bool `default:"false" describe:"启用Elasticsearch"`
+ EsUrl string `default:"" describe:"Elasticsearch url"`
+ EsIndex string `default:"server_log_v1" describe:"Elasticsearch index"`
+ EsUsername string `default:"" describe:"Elasticsearch用户名"`
+ EsPassword string `default:"" describe:"Elasticsearch密码"`
+ FileSizeLimit float64 `default:"10.0" describe:"文件大小限制(MB)"`
+ ProcessInputPrefix string `default:">" describe:"进程输入前缀"`
+ ProcessRestartsLimit int `default:"2" describe:"进程重启次数限制"`
+ ProcessMsgCacheLinesLimit int `default:"50" describe:"std进程缓存消息行数"`
+ ProcessMsgCacheBufLimit int `default:"4096" describe:"pty进程缓存消息字节长度"`
+ ProcessExpireTime int64 `default:"60" describe:"进程控制权过期时间(秒)"`
+ PerformanceInfoListLength int `default:"30" describe:"性能信息存储长度"`
+ PerformanceInfoInterval int `default:"1" describe:"监控获取间隔时间(分钟)"`
+ UserPassWordMinLength int `default:"4" describe:"用户密码最小长度"`
+ LogMinLenth int `default:"0" describe:"过滤日志最小长度"`
+ PprofEnable bool `default:"true" describe:"启用pprof分析工具"`
+}
diff --git a/consts/consts.go b/consts/consts.go
new file mode 100644
index 0000000..289ea05
--- /dev/null
+++ b/consts/consts.go
@@ -0,0 +1 @@
+package consts
\ No newline at end of file
diff --git a/consts/ctxflag/ctxflag.go b/consts/ctxflag/ctxflag.go
new file mode 100644
index 0000000..9717049
--- /dev/null
+++ b/consts/ctxflag/ctxflag.go
@@ -0,0 +1,7 @@
+package ctxflag
+
+const (
+ USER_NAME = "user"
+ ROLE = "role"
+ ERR = "err"
+)
diff --git a/consts/permission/permission.go b/consts/permission/permission.go
new file mode 100644
index 0000000..343acb1
--- /dev/null
+++ b/consts/permission/permission.go
@@ -0,0 +1,9 @@
+package permission
+
+type OprPermission string
+
+const (
+ START_OPERATION OprPermission = "Start"
+ STOP_OPERATION OprPermission = "Stop"
+ TERMINAL_OPERATION OprPermission = "Terminal"
+)
diff --git a/consts/role/role.go b/consts/role/role.go
new file mode 100644
index 0000000..ef9db76
--- /dev/null
+++ b/consts/role/role.go
@@ -0,0 +1,10 @@
+package role
+
+type Role int
+
+const (
+ ROOT Role = iota
+ ADMIN
+ USER
+ GUEST
+)
diff --git a/dao/config.go b/dao/config.go
new file mode 100644
index 0000000..579fc9d
--- /dev/null
+++ b/dao/config.go
@@ -0,0 +1,30 @@
+package dao
+
+import (
+ "msm/model"
+
+ "gorm.io/gorm"
+)
+
+type configDao struct{}
+
+var ConfigDao = new(configDao)
+
+func (c *configDao) GetConfigValue(key string) (string, error) {
+ var result string
+ if err := db.Model(&model.Config{}).Select("value").Where("key = ?", key).First(&result).Error; err != nil {
+ return "", err
+ }
+ return result, nil
+}
+
+func (c *configDao) SetConfigValue(key, value string) error {
+ if db.Model(&model.Config{}).Where("key = ?", key).First(nil).Error == gorm.ErrRecordNotFound {
+ return db.Create(&model.Config{
+ Key: key,
+ Value: value,
+ }).Error
+ } else {
+ return db.Model(&model.Config{}).Where("key = ?", key).Updates(model.Config{Value: value}).Error
+ }
+}
diff --git a/dao/db.go b/dao/db.go
new file mode 100644
index 0000000..6034630
--- /dev/null
+++ b/dao/db.go
@@ -0,0 +1,40 @@
+package dao
+
+import (
+ "log"
+ zlog "msm/log"
+ "msm/model"
+ "os"
+ "time"
+
+ "github.com/glebarez/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+)
+
+var db *gorm.DB
+
+var defaultConfig = gorm.Session{PrepareStmt: true, SkipDefaultTransaction: true}
+
+func init() {
+ newLogger := logger.New(
+ log.New(os.Stdout, "\r\n", log.LstdFlags),
+ logger.Config{
+ SlowThreshold: time.Second,
+ LogLevel: logger.Silent,
+ IgnoreRecordNotFoundError: true,
+ ParameterizedQueries: true,
+ Colorful: true,
+ },
+ )
+ gdb, err := gorm.Open(sqlite.Open("data.db"), &gorm.Config{
+ Logger: newLogger,
+ })
+ if err != nil {
+ zlog.Logger.Panicf("sqlite数据库初始化失败!\n错误原因:%v", err)
+ }
+ zlog.Logger.Info("sqlite初始化成功")
+ db = gdb.Session(&defaultConfig)
+ // db = gdb.Session(&defaultConfig).Debug()
+ db.AutoMigrate(&model.Process{}, &model.User{}, &model.Permission{}, &model.Push{}, &model.Config{})
+}
diff --git a/dao/permission.go b/dao/permission.go
new file mode 100644
index 0000000..472456f
--- /dev/null
+++ b/dao/permission.go
@@ -0,0 +1,42 @@
+package dao
+
+import (
+ "msm/log"
+ "msm/model"
+
+ "gorm.io/gorm"
+)
+
+type permissionDao struct{}
+
+var PermissionDao = new(permissionDao)
+
+func (p *permissionDao) GetPermssionList(account string) []model.PermissionPo {
+ result := []model.PermissionPo{}
+ if err := db.Raw(`SELECT p.name ,p.uuid as pid,p2.owned ,p2."start" ,p2.stop ,p2.terminal
+ FROM users u full join process p left join permission p2 on p2.account == u.account and p2.pid =p.uuid WHERE u.account = ? or u.account ISNULL`, account).Find(&result); err.Error != nil {
+ log.Logger.Warnw("权限查询失败", "err", err)
+ }
+
+ return result
+}
+
+func (p *permissionDao) EditPermssion(data model.Permission) error {
+ if db.Model(&model.Permission{}).Where("account = ? and pid = ?", data.Account, data.Pid).First(nil).Error == gorm.ErrRecordNotFound {
+ db.Omit("name").Create(&model.Permission{
+ Account: data.Account,
+ Pid: data.Pid,
+ })
+ }
+ return db.Debug().Model(&model.Permission{}).Where("account = ? and pid = ?", data.Account, data.Pid).Updates(map[string]interface{}{
+ "owned": data.Owned,
+ "start": data.Start,
+ "stop": data.Stop,
+ "terminal": data.Terminal,
+ }).Error
+}
+
+func (p *permissionDao) GetPermission(user string, pid int) (result model.Permission) {
+ db.Debug().Model(&model.Permission{}).Where("account = ? and pid = ?", user, pid).First(&result)
+ return
+}
diff --git a/dao/process.go b/dao/process.go
new file mode 100644
index 0000000..1cf3313
--- /dev/null
+++ b/dao/process.go
@@ -0,0 +1,58 @@
+package dao
+
+import (
+ "msm/log"
+ "msm/model"
+)
+
+type processDao struct{}
+
+var ProcessDao = new(processDao)
+
+func (p *processDao) GetAllProcessConfig() []model.Process {
+ result := []model.Process{}
+
+ tx := db.Find(&result)
+ if tx.Error != nil {
+ log.Logger.Error(tx.Error)
+ return []model.Process{}
+ }
+ return result
+}
+
+func (p *processDao) GetProcessConfigByUser(username string) []model.Process {
+ result := []model.Process{}
+ tx := db.Debug().Raw(`SELECT p.uuid, p.name FROM permission left join process p where pid =p.uuid and owned = 1 and account = ?`, username).Scan(&result)
+ if tx.Error != nil {
+ log.Logger.Error(tx.Error)
+ return []model.Process{}
+ }
+ return result
+}
+
+func (p *processDao) UpdateProcessConfig(process model.Process) error {
+ tx := db.Save(&process)
+ return tx.Error
+}
+
+func (p *processDao) AddProcessConfig(process model.Process) (int, error) {
+ tx := db.Create(&process)
+ return process.Uuid, tx.Error
+}
+
+func (p *processDao) DeleteProcessConfig(uuid int) error {
+ tx := db.Delete(&model.Process{
+ Uuid: uuid,
+ })
+ return tx.Error
+}
+
+func (p *processDao) GetProcessConfigById(uuid int) model.Process {
+ result := model.Process{}
+ tx := db.Where(&model.Process{Uuid: uuid}).First(&result)
+ if tx.Error != nil {
+ log.Logger.Error(tx.Error)
+ return model.Process{}
+ }
+ return result
+}
diff --git a/dao/push.go b/dao/push.go
new file mode 100644
index 0000000..3c88ebd
--- /dev/null
+++ b/dao/push.go
@@ -0,0 +1,33 @@
+package dao
+
+import (
+ "msm/model"
+)
+
+type pushDao struct{}
+
+var PushDao = new(pushDao)
+
+func (p *pushDao) GetPushList() (result []model.Push) {
+ db.Find(&result)
+ return
+}
+
+func (p *pushDao) GetPushConfigById(id int) (result model.Push) {
+ db.Where("id = ?", id).First(&result)
+ return
+}
+
+func (p *pushDao) UpdatePushConfig(data model.Push) error {
+ return db.Save(&data).Error
+}
+
+func (p *pushDao) AddPushConfig(data model.Push) error {
+ return db.Create(&data).Error
+}
+
+func (p *pushDao) DeletePushConfig(id int) error {
+ return db.Delete(&model.Push{
+ Id: int64(id),
+ }).Error
+}
diff --git a/dao/user.go b/dao/user.go
new file mode 100644
index 0000000..bbb8342
--- /dev/null
+++ b/dao/user.go
@@ -0,0 +1,51 @@
+package dao
+
+import (
+ "errors"
+ "msm/config"
+ "msm/model"
+ "msm/utils"
+ "time"
+)
+
+type userDao struct{}
+
+var UserDao = new(userDao)
+
+func (u *userDao) GetUserByName(name string) model.User {
+ var result model.User
+ db.Where("account = ?", name).First(&result)
+ return result
+}
+
+func (u *userDao) CreateUser(user model.User) error {
+ if len(user.Password) < config.CF.UserPassWordMinLength {
+ return errors.New("密码小于最小长度")
+ }
+ user.Password = utils.Md5(user.Password)
+ user.CreateTime = time.Now()
+ tx := db.Create(&user)
+ return tx.Error
+}
+
+func (u *userDao) UpdatePassword(name string, password string) error {
+ if len(password) < config.CF.UserPassWordMinLength {
+ return errors.New("新密码太短")
+ }
+ tx := db.Model(&model.User{}).Where("account = ?", name).Update("password", utils.Md5(password))
+ return tx.Error
+}
+
+func (u *userDao) DeleteUser(name string) error {
+ if err := db.Where("account = ?", name).First(&model.User{}).Error; err != nil {
+ return err
+ }
+ tx := db.Delete(&model.User{Account: name})
+ return tx.Error
+}
+
+func (u *userDao) GetUserList() []model.User {
+ result := []model.User{}
+ db.Find(&result)
+ return result
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..dad51d8
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,83 @@
+module msm
+
+go 1.21.1
+
+require github.com/gorilla/websocket v1.5.1
+
+require (
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/tklauser/go-sysconf v0.3.13 // indirect
+ github.com/tklauser/numcpus v0.7.0 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+)
+
+require (
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ gorm.io/gorm v1.25.7
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/elastic/elastic-transport-go/v8 v8.5.0 // indirect
+ github.com/glebarez/go-sqlite v1.21.2 // indirect
+ github.com/go-logr/logr v1.3.0 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/google/go-querystring v1.1.0 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ go.opentelemetry.io/otel v1.21.0 // indirect
+ go.opentelemetry.io/otel/metric v1.21.0 // indirect
+ go.opentelemetry.io/otel/trace v1.21.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+ modernc.org/libc v1.22.5 // indirect
+ modernc.org/mathutil v1.5.0 // indirect
+ modernc.org/memory v1.5.0 // indirect
+ modernc.org/sqlite v1.23.1 // indirect
+)
+
+require (
+ github.com/bytedance/sonic v1.11.6 // indirect
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/gin-contrib/pprof v1.5.0 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
+ go.uber.org/multierr v1.10.0 // indirect
+ golang.org/x/arch v0.8.0 // indirect
+ golang.org/x/crypto v0.22.0 // indirect
+ golang.org/x/sys v0.20.0 // indirect
+ golang.org/x/text v0.15.0 // indirect
+ google.golang.org/protobuf v1.34.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
+require (
+ github.com/creack/pty v1.1.21
+ github.com/elastic/go-elasticsearch/v8 v8.13.1
+ github.com/gin-gonic/gin v1.9.1
+ github.com/glebarez/sqlite v1.11.0
+ github.com/golang-jwt/jwt v3.2.2+incompatible
+ github.com/levigross/grequests v0.0.0-20231203190023-9c307ef1f48d
+ github.com/panjf2000/ants v1.3.0
+ github.com/shirou/gopsutil v3.21.11+incompatible
+ go.uber.org/zap v1.26.0
+ golang.org/x/net v0.24.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..76162ad
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,212 @@
+github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
+github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
+github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/elastic/elastic-transport-go/v8 v8.5.0 h1:v5membAl7lvQgBTexPRDBO/RdnlQX+FM9fUVDyXxvH0=
+github.com/elastic/elastic-transport-go/v8 v8.5.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
+github.com/elastic/go-elasticsearch/v8 v8.13.1 h1:du5F8IzUUyCkzxyHdrO9AtopcG95I/qwi2WK8Kf1xlg=
+github.com/elastic/go-elasticsearch/v8 v8.13.1/go.mod h1:DIn7HopJs4oZC/w0WoJR13uMUxtHeq92eI5bqv5CRfI=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYVrU=
+github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
+github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
+github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
+github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
+github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
+github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
+github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/levigross/grequests v0.0.0-20231203190023-9c307ef1f48d h1:8fVmm2qScPn4JAF/YdTtqrPP3n58FgZ4GbKTNfaPuRs=
+github.com/levigross/grequests v0.0.0-20231203190023-9c307ef1f48d/go.mod h1:dFu6nuJHC3u9kCDcyGrEL7LwhK2m6Mt+alyiiIjDrRY=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/panjf2000/ants v1.3.0 h1:8pQ+8leaLc9lys2viEEr8md0U4RN6uOSUCE9bOYjQ9M=
+github.com/panjf2000/ants v1.3.0/go.mod h1:AaACblRPzq35m1g3enqYcxspbbiOJJYaxU2wMpm1cXY=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
+github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
+github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
+github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
+github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
+go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
+go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
+go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
+go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
+go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
+go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
+google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
+modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
+modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
+modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
+modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
+modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/log/log.go b/log/log.go
new file mode 100644
index 0000000..d08ef8d
--- /dev/null
+++ b/log/log.go
@@ -0,0 +1,44 @@
+package log
+
+import (
+ "log"
+ "msm/config"
+
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+var Logger *zap.SugaredLogger
+
+func init() {
+ encoderConfig := zapcore.EncoderConfig{
+ TimeKey: "time",
+ LevelKey: "level",
+ NameKey: "logger",
+ CallerKey: "caller",
+ MessageKey: "msg",
+ StacktraceKey: "stacktrace",
+ LineEnding: zapcore.DefaultLineEnding,
+ EncodeLevel: zapcore.CapitalColorLevelEncoder,
+ EncodeTime: zapcore.ISO8601TimeEncoder,
+ EncodeDuration: zapcore.SecondsDurationEncoder,
+ EncodeCaller: zapcore.FullCallerEncoder,
+ }
+ level, err := zapcore.ParseLevel(config.CF.LogLevel)
+ if err != nil {
+ log.Printf("日志等级错误!不存在“%v”日志等级", config.CF.LogLevel)
+ level = zap.DebugLevel
+ }
+ atom := zap.NewAtomicLevelAt(level)
+ zap.NewDevelopmentConfig()
+ config := zap.Config{
+ Level: atom,
+ Development: true,
+ Encoding: "console",
+ EncoderConfig: encoderConfig,
+ OutputPaths: []string{"stdout", "info.log"},
+ ErrorOutputPaths: []string{"stderr"},
+ }
+ log, _ := config.Build()
+ Logger = log.Sugar()
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..9e0eb39
--- /dev/null
+++ b/main.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+ "msm/boot"
+ "msm/route"
+
+ "github.com/gin-gonic/gin"
+)
+
+func main() {
+ boot.Boot()
+ // go termui.TermuiInit()
+ gin.SetMode(gin.ReleaseMode)
+ route.Route()
+}
diff --git a/model/config.go b/model/config.go
new file mode 100644
index 0000000..f49cc68
--- /dev/null
+++ b/model/config.go
@@ -0,0 +1,18 @@
+package model
+
+type Config struct {
+ Id int `gorm:"column:id;primary_key"`
+ Key string `gorm:"column:key"`
+ Value string `gorm:"column:value"`
+}
+
+func (n *Config) TableName() string {
+ return "config"
+}
+
+type SystemConfigurationResp struct {
+ Key string `json:"key"`
+ Value any `json:"value"`
+ Default string `json:"default"`
+ Describe string `json:"describe"`
+}
diff --git a/model/es.go b/model/es.go
new file mode 100644
index 0000000..99a70cb
--- /dev/null
+++ b/model/es.go
@@ -0,0 +1,103 @@
+package model
+
+type EsResult struct {
+ Took int `json:"took"`
+ TimedOut bool `json:"timed_out"`
+ Shards Shards `json:"_shards"`
+ Hits Hits `json:"hits"`
+}
+type Shards struct {
+ Total int `json:"total"`
+ Successful int `json:"successful"`
+ Skipped int `json:"skipped"`
+ Failed int `json:"failed"`
+}
+type Total struct {
+ Value int `json:"value"`
+ Relation string `json:"relation"`
+}
+type Source struct {
+ Log string `json:"log"`
+ Name string `json:"name"`
+ Time int64 `json:"time"`
+ Using string `json:"using"`
+}
+type HitsItem struct {
+ Index string `json:"_index"`
+ ID string `json:"_id"`
+ Score interface{} `json:"_score"`
+ Source Source `json:"_source"`
+ Sort []int64 `json:"sort"`
+}
+type Hits struct {
+ Total Total `json:"total"`
+ MaxScore interface{} `json:"max_score"`
+ Hits []HitsItem `json:"hits"`
+}
+
+type GetLogReq struct {
+ Match struct {
+ Log string `json:"log"`
+ Name string `json:"name"`
+ Using string `json:"using"`
+ } `json:"match"`
+ TimeRange struct {
+ StartTime int64 `json:"startTime"`
+ EndTime int64 `json:"endTime"`
+ } `json:"time"`
+ Page struct {
+ From int `json:"from"`
+ Size int `json:"size"`
+ } `json:"page"`
+ Sort string `json:"sort"`
+}
+
+type EsResp struct {
+ Took int `json:"took"`
+ TimedOut bool `json:"timed_out"`
+ Shards struct {
+ Total int `json:"total"`
+ Successful int `json:"successful"`
+ Skipped int `json:"skipped"`
+ Failed int `json:"failed"`
+ } `json:"_shards"`
+ Hits struct {
+ Total struct {
+ Value int `json:"value"`
+ Relation string `json:"relation"`
+ } `json:"total"`
+ MaxScore int `json:"max_score"`
+ Hits []struct {
+ Index string `json:"_index"`
+ ID string `json:"_id"`
+ Score int `json:"_score"`
+ Source struct {
+ Log string `json:"log"`
+ Name string `json:"name"`
+ Time int64 `json:"time"`
+ Using string `json:"using"`
+ } `json:"_source"`
+ } `json:"hits"`
+ } `json:"hits"`
+}
+
+type LogResp struct {
+ Total int `json:"total"`
+ Data []Eslog `json:"data"`
+}
+
+type Eslog struct {
+ Log string `json:"log"`
+ Time int64 `json:"time"`
+ Name string `json:"name"`
+ Using string `json:"using"`
+ Id string `json:"id"`
+}
+
+type QueryBody struct {
+ Query struct {
+ Bool struct {
+ Must []any `json:"must"`
+ } `json:"bool"`
+ } `json:"query"`
+}
diff --git a/model/file.go b/model/file.go
new file mode 100644
index 0000000..d0f12e7
--- /dev/null
+++ b/model/file.go
@@ -0,0 +1,6 @@
+package model
+
+type FileStruct struct {
+ Name string `json:"name"`
+ IsDir bool `json:"isDir"`
+}
diff --git a/model/message.go b/model/message.go
new file mode 100644
index 0000000..5f542c5
--- /dev/null
+++ b/model/message.go
@@ -0,0 +1,8 @@
+package model
+
+
+
+type WsMessage struct {
+ MessageType string `json:"messageType"`
+ Content string `json:"content"`
+}
diff --git a/model/permission.go b/model/permission.go
new file mode 100644
index 0000000..448fbab
--- /dev/null
+++ b/model/permission.go
@@ -0,0 +1,26 @@
+package model
+
+type Permission struct {
+ Id int64 `gorm:"column:id;NOT NULL" json:"id"`
+ Account string `gorm:"column:account;NOT NULL" json:"account"`
+ Pid int32 `gorm:"column:pid;NOT NULL" json:"pid"`
+ Owned bool `gorm:"column:owned;NOT NULL" json:"owned"`
+ Start bool `gorm:"column:start;NOT NULL" json:"start"`
+ Stop bool `gorm:"column:stop;NOT NULL" json:"stop"`
+ Terminal bool `gorm:"column:terminal;NOT NULL" json:"terminal"`
+}
+
+func (*Permission) TableName() string {
+ return "permission"
+}
+
+type PermissionPo struct {
+ Id int64 `gorm:"column:id" json:"id"`
+ Account string `gorm:"column:account" json:"account"`
+ Name string `gorm:"column:name" json:"name"`
+ Pid int32 `gorm:"column:pid" json:"pid"`
+ Owned bool `gorm:"column:owned" json:"owned"`
+ Start bool `gorm:"column:start" json:"start"`
+ Stop bool `gorm:"column:stop" json:"stop"`
+ Terminal bool `gorm:"column:terminal" json:"terminal"`
+}
diff --git a/model/proc.go b/model/proc.go
new file mode 100644
index 0000000..de0b144
--- /dev/null
+++ b/model/proc.go
@@ -0,0 +1,22 @@
+package model
+
+type ProcessInfo struct {
+ Name string `json:"name"`
+ Uuid int `json:"uuid"`
+ StartTime string `json:"startTime"`
+ User string `json:"user"`
+ Usage Usage `json:"usage"`
+ State State `json:"state"`
+ TermType string `json:"termType"`
+}
+
+type Usage struct {
+ Cpu []float64 `json:"cpu"`
+ Mem []float64 `json:"mem"`
+ Time []string `json:"time"`
+}
+
+type State struct {
+ State uint8 `json:"state"`
+ Info string `json:"info"`
+}
diff --git a/model/process.go b/model/process.go
new file mode 100644
index 0000000..16ac218
--- /dev/null
+++ b/model/process.go
@@ -0,0 +1,16 @@
+package model
+
+type Process struct {
+ Uuid int `gorm:"primaryKey;autoIncrement;column:uuid" json:"uuid"`
+ Name string `gorm:"column:name" json:"name"`
+ Cmd string `gorm:"column:args" json:"cmd"`
+ Cwd string `gorm:"column:cwd" json:"cwd"`
+ AutoRestart bool `gorm:"column:auto_restart" json:"autoRestart"`
+ Push bool `gorm:"column:push" json:"push"`
+ LogReport bool `gorm:"column:log_report" json:"logReport"`
+ TermType string `gorm:"column:term_type" json:"termType"`
+}
+
+func (*Process) TableName() string {
+ return "process"
+}
diff --git a/model/push_msg.go b/model/push_msg.go
new file mode 100644
index 0000000..090dc9c
--- /dev/null
+++ b/model/push_msg.go
@@ -0,0 +1,14 @@
+package model
+
+type Push struct {
+ Id int64 `gorm:"column:id;NOT NULL" json:"id"`
+ Method string `gorm:"column:method;NOT NULL" json:"method"`
+ Url string `gorm:"column:url;NOT NULL" json:"url"`
+ Body string `gorm:"column:body;NOT NULL" json:"body"`
+ Remark string `gorm:"column:remark;NOT NULL" json:"remark"`
+ Enable bool `gorm:"column:enable;NOT NULL" json:"enable"`
+}
+
+func (*Push) TableName() string {
+ return "push"
+}
diff --git a/model/user.go b/model/user.go
new file mode 100644
index 0000000..538f713
--- /dev/null
+++ b/model/user.go
@@ -0,0 +1,15 @@
+package model
+
+import "time"
+
+type User struct {
+ Account string `json:"account" gorm:"primaryKey;column:account" `
+ Password string `json:"password" gorm:"column:password" `
+ Role int `json:"role" gorm:"column:role" `
+ CreateTime time.Time `json:"createTime" gorm:"column:create_time" `
+ Remark string `json:"remark" gorm:"column:remark" `
+}
+
+func (*User) TableName() string {
+ return "users"
+}
diff --git a/route/middle/panic.go b/route/middle/panic.go
new file mode 100644
index 0000000..5fd8b65
--- /dev/null
+++ b/route/middle/panic.go
@@ -0,0 +1,26 @@
+package middle
+
+import (
+ "msm/consts/ctxflag"
+
+ "github.com/gin-gonic/gin"
+)
+
+func PanicMiddle() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ defer func() {
+ if err := recover(); err == 0 {
+ if err, ok := c.Get(ctxflag.ERR); ok {
+ rErr(c, -1, err.(error).Error(), err.(error))
+ } else {
+ rErr(c, -1, "内部错误", nil)
+ }
+ } else {
+ if err != nil {
+ panic(err)
+ }
+ }
+ }()
+ c.Next()
+ }
+}
diff --git a/route/middle/permission.go b/route/middle/permission.go
new file mode 100644
index 0000000..35ced67
--- /dev/null
+++ b/route/middle/permission.go
@@ -0,0 +1,44 @@
+package middle
+
+import (
+ "msm/consts/ctxflag"
+ "msm/consts/permission"
+ "msm/consts/role"
+ "msm/dao"
+ "reflect"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+func RolePermission(needPermission role.Role) func(ctx *gin.Context) {
+ return func(ctx *gin.Context) {
+ if r := ctx.GetInt(ctxflag.ROLE); r > int(needPermission) {
+ rErr(ctx, -1, "角色权限不足", nil)
+ ctx.Abort()
+ return
+ }
+ ctx.Next()
+ }
+}
+
+func OprPermission(op permission.OprPermission) func(ctx *gin.Context) {
+ return func(ctx *gin.Context) {
+ uuid, err := strconv.Atoi(ctx.Query("uuid"))
+ if err != nil {
+ rErr(ctx, -1, "参数有误", nil)
+ ctx.Abort()
+ return
+ }
+ if ctx.GetInt(ctxflag.ROLE) < int(role.USER) {
+ ctx.Next()
+ return
+ }
+ if !reflect.ValueOf(dao.PermissionDao.GetPermission(ctx.GetString(ctxflag.USER_NAME), uuid)).FieldByName(string(op)).Bool() {
+ rErr(ctx, -1, "操作权限不足", nil)
+ ctx.Abort()
+ return
+ }
+ ctx.Next()
+ }
+}
diff --git a/route/middle/token.go b/route/middle/token.go
new file mode 100644
index 0000000..e6ca3ca
--- /dev/null
+++ b/route/middle/token.go
@@ -0,0 +1,73 @@
+package middle
+
+import (
+ "errors"
+ "msm/consts/ctxflag"
+ "msm/dao"
+ "msm/log"
+ "msm/utils"
+ "slices"
+
+ "github.com/gin-gonic/gin"
+)
+
+// code -1为失败,-2为token失效
+func rErr(ctx *gin.Context, code int, message string, err error) {
+ var statusCode int
+ switch code {
+ case -1:
+ statusCode = 500
+ case -2:
+ statusCode = 401
+ default:
+ statusCode = 200
+ }
+ log.Logger.Warn(err)
+ ctx.JSON(statusCode, map[string]any{
+ "code": code,
+ "msg": message,
+ })
+ ctx.Abort()
+}
+
+func CheckToken() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ whiteList := []string{
+ "/api/user/login",
+ "/api/user/register/admin",
+ }
+ if !slices.Contains(whiteList, c.Request.URL.Path) {
+ var token string
+ if c.Request.Header.Get("token") != "" {
+ token = c.Request.Header.Get("token")
+ } else {
+ token = c.Query("token")
+ }
+ if _, err := utils.ParseToken(token); err != nil {
+ rErr(c, -2, "token校验失败", err)
+ return
+ }
+ if username, err := getUser(c); err != nil {
+ rErr(c, -1, "无法获取user信息", err)
+ } else {
+ c.Set(ctxflag.USER_NAME, username)
+ c.Set(ctxflag.ROLE, dao.UserDao.GetUserByName(username).Role)
+ }
+ }
+ c.Next()
+ }
+}
+
+func getUser(ctx *gin.Context) (string, error) {
+ var token string
+ if ctx.Request.Header.Get("token") != "" {
+ token = ctx.Request.Header.Get("token")
+ } else {
+ token = ctx.Query("token")
+ }
+ if mc, err := utils.ParseToken(token); err == nil && mc != nil {
+ return mc.UserName, nil
+ } else {
+ return "", errors.Join(errors.New("用户信息获取失败"), err)
+ }
+}
diff --git a/route/route.go b/route/route.go
new file mode 100644
index 0000000..85cfd55
--- /dev/null
+++ b/route/route.go
@@ -0,0 +1,111 @@
+package route
+
+import (
+ "io"
+ "msm/api"
+ "msm/config"
+ "msm/consts/permission"
+ "msm/consts/role"
+ "msm/log"
+ "msm/route/middle"
+ "net/http"
+
+ "github.com/gin-contrib/pprof"
+ "github.com/gin-gonic/gin"
+)
+
+func Route() {
+ r := gin.Default()
+ gin.DefaultWriter = io.Discard
+ gin.SetMode(gin.DebugMode)
+ routePathInit(r)
+ staticInit(r)
+ pprofInit(r)
+ r.Run(config.CF.Listen)
+}
+
+func staticInit(r *gin.Engine) {
+ r.NoRoute(func(c *gin.Context) {
+ c.HTML(http.StatusOK, "index.html", gin.H{})
+ })
+ r.Static("/js", "templates/js")
+ r.Static("/css", "templates/css")
+ r.Static("/media", "templates/media")
+ r.Static("/fonts", "templates/fonts")
+ r.LoadHTMLFiles("templates/index.html")
+}
+
+func pprofInit(r *gin.Engine) {
+ if config.CF.PprofEnable {
+ pprof.Register(r)
+ log.Logger.Info("启用 pprof")
+ }
+}
+
+func routePathInit(r *gin.Engine) {
+ apiGroup := r.Group("/api")
+ apiGroup.Use(middle.CheckToken())
+ apiGroup.Use(middle.PanicMiddle())
+ {
+ apiGroup.GET("/ws", middle.OprPermission(permission.TERMINAL_OPERATION), api.WsApi.WebsocketHandle)
+
+ processGroup := apiGroup.Group("/process")
+ {
+ processGroup.DELETE("", middle.OprPermission(permission.STOP_OPERATION), api.ProcApi.KillProcess)
+ processGroup.GET("", api.ProcApi.GetProcessList)
+ processGroup.PUT("", middle.OprPermission(permission.START_OPERATION), api.ProcApi.StartProcess)
+ processGroup.GET("/control", middle.RolePermission(role.ADMIN), api.ProcApi.ProcessControl)
+
+ proConfigGroup := processGroup.Group("/config")
+ {
+ proConfigGroup.POST("", middle.RolePermission(role.ROOT), api.ProcApi.CreateNewProcess)
+ proConfigGroup.DELETE("", middle.RolePermission(role.ROOT), api.ProcApi.DeleteNewProcess)
+ proConfigGroup.PUT("", middle.RolePermission(role.ROOT), api.ProcApi.UpdateProcessConfig)
+ proConfigGroup.GET("", middle.RolePermission(role.ADMIN), api.ProcApi.GetProcessConfig)
+ }
+ }
+
+ userGroup := apiGroup.Group("/user")
+ {
+ userGroup.POST("/login", api.UserApi.LoginHandler)
+ userGroup.POST("", middle.RolePermission(role.ROOT), api.UserApi.CreateUser)
+ userGroup.PUT("/password", middle.RolePermission(role.USER), api.UserApi.ChangePassword)
+ userGroup.DELETE("", middle.RolePermission(role.ROOT), api.UserApi.DeleteUser)
+ userGroup.GET("", middle.RolePermission(role.ROOT), api.UserApi.GetUserList)
+ }
+
+ pushGroup := apiGroup.Group("/push").Use(middle.RolePermission(role.ADMIN))
+ {
+ pushGroup.GET("/list", api.PushApi.GetPushList)
+ pushGroup.GET("", api.PushApi.GetPushById)
+ pushGroup.POST("", api.PushApi.AddPushConfig)
+ pushGroup.PUT("", api.PushApi.UpdatePushConfig)
+ pushGroup.DELETE("", api.PushApi.DeletePushConfig)
+ }
+
+ fileGroup := apiGroup.Group("/file").Use(middle.RolePermission(role.ADMIN))
+ {
+ fileGroup.GET("/list", api.FileApi.FilePathHandler)
+ fileGroup.PUT("", api.FileApi.FileWriteHandler)
+ fileGroup.GET("", api.FileApi.FileReadHandler)
+ }
+
+ permissionGroup := apiGroup.Group("/permission").Use(middle.RolePermission(role.ROOT))
+ {
+ permissionGroup.GET("/list", api.PermissionApi.GetPermissionList)
+ permissionGroup.PUT("", api.PermissionApi.EditPermssion)
+ }
+
+ logGroup := apiGroup.Group("/log").Use(middle.RolePermission(role.ADMIN))
+ {
+ logGroup.POST("", api.LogApi.GetLog)
+ }
+
+ configGroup := apiGroup.Group("/config").Use(middle.RolePermission(role.ROOT))
+ {
+ configGroup.GET("", api.ConfigApi.GetSystemConfiguration)
+ configGroup.PUT("", api.ConfigApi.SetSystemConfiguration)
+ configGroup.PUT("/es", api.ConfigApi.EsConfigReload)
+ }
+ }
+}
diff --git a/server_config.json b/server_config.json
new file mode 100644
index 0000000..3a72672
--- /dev/null
+++ b/server_config.json
@@ -0,0 +1,128 @@
+{
+ "user": [
+ {
+ "account": "admin",
+ "password": "91cd3b960f31c06fc4048ff44e6654c1"
+ },
+ {
+ "account": "user",
+ "password": "5f4dcc3b5aa765d61d8327deb882cf99"
+ }
+ ],
+ "server": [
+ {
+ "name": "bungeecord",
+ "args": [
+ "/usr/lib/jvm/java-17-openjdk-amd64/bin/java",
+ "-Xmx580M",
+ "-Xms100M",
+ "-jar",
+ "waterfall-1.19-510.jar"
+ ],
+ "cwd": "/MCS/BungeeCord",
+ "autoRestart": true,
+ "push": true,
+ "logReport": true
+ },
+ {
+ "name": "lobby",
+ "args": [
+ "java",
+ "-jar",
+ "-server",
+ "-Xmx1000M",
+ "paper-1.16.5-794.jar"
+ ],
+ "cwd": "/MCS/lobby",
+ "autoRestart": true,
+ "push": true,
+ "logReport": true
+ },
+ {
+ "name": "main",
+ "args": [
+ "java",
+ "-jar",
+ "-server",
+ "-Xmx8000M",
+ "launcher-airplane.jar"
+ ],
+ "cwd": "/MCS",
+ "autoRestart": true,
+ "push": true,
+ "logReport": true
+ },
+ {
+ "name": "s1",
+ "args": [
+ "java",
+ "-jar",
+ "-server",
+ "-Xmx5000M",
+ "launcher-airplane.jar"
+ ],
+ "cwd": "/MCS/server1",
+ "autoRestart": true,
+ "push": true,
+ "logReport": true
+ },
+ {
+ "name": "起床战争bungeecord",
+ "args": [
+ "java",
+ "-jar",
+ "BB.jar"
+ ],
+ "cwd": "/MCS/bed/[25565]BungeeCord",
+ "autoRestart": true
+ },
+ {
+ "name": "起床战争looby",
+ "args": [
+ "java",
+ "-jar",
+ "paper-1.16.5-794.jar"
+ ],
+ "cwd": "/MCS/bed/[25566]Lobby",
+ "autoRestart": true
+ },
+ {
+ "name": "起床战争入侵",
+ "args": [
+ "/usr/lib/jvm/java-8-openjdk-amd64/bin/java",
+ "-jar",
+ "RQS.jar"
+ ],
+ "cwd": "/MCS/bed/[20003]BedWars-入侵",
+ "autoRestart": true
+ },
+ {
+ "name": "起床战争蘑菇",
+ "args": [
+ "/usr/lib/jvm/java-8-openjdk-amd64/bin/java",
+ "-jar",
+ "PaperSpigot-1.8.8.jar"
+ ],
+ "cwd": "/MCS/bed/[10002]BedWarsXP-蘑菇",
+ "autoRestart": true
+ }
+ ],
+ "mq": {
+ "enable": true,
+ "mqurl": "amqp://admin:1625167628%40xcon@xcon.top:5672/",
+ "queue_name": "log_queue_publisher",
+ "exchange": "log_exchange_publisher",
+ "routing_key": "server_log"
+ },
+ "push": {
+ "feishu": {
+ "enable": true,
+ "webhook": "https://open.feishu.cn/open-apis/bot/v2/hook/86f491ba-6c7a-413b-86e6-d72420934bd9"
+ },
+ "wechat":{
+ "enable": true,
+ "webhook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=5f3fae11-da5c-45a7-8ae9-ceaca2dc1495"
+ }
+ },
+ "logLevel": "debug"
+}
\ No newline at end of file
diff --git a/service/es/es.go b/service/es/es.go
new file mode 100644
index 0000000..7809cdf
--- /dev/null
+++ b/service/es/es.go
@@ -0,0 +1,155 @@
+package es
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "msm/config"
+ "msm/log"
+ "msm/model"
+
+ "github.com/elastic/go-elasticsearch/v8"
+ "github.com/elastic/go-elasticsearch/v8/esapi"
+)
+
+var esClient *elasticsearch.Client
+
+type esService struct{}
+
+var EsService = new(esService)
+
+func InitEs() bool {
+ if config.CF.EsEnable {
+ cfg := elasticsearch.Config{
+ Addresses: []string{
+ config.CF.EsUrl,
+ },
+ Username: config.CF.EsUsername,
+ Password: config.CF.EsPassword,
+ }
+ var err error
+ esClient, err = elasticsearch.NewClient(cfg)
+ if err != nil {
+ log.Logger.Fatalln("Failed to connect to es")
+ }
+ _, err = esClient.Info()
+ if err != nil {
+ log.Logger.Error("es启动失败", err)
+ config.CF.EsEnable = false
+ } else {
+ return true
+ }
+ } else {
+ log.Logger.Debug("不使用es")
+ }
+ return false
+}
+
+// idx 为空,默认随机唯一字符串
+func (e *esService) Index(index, idx string, doc map[string]interface{}) {
+ var buf bytes.Buffer
+ if err := json.NewEncoder(&buf).Encode(doc); err != nil {
+ log.Logger.Error(err, "Error encoding doc")
+ return
+ }
+ res, err := esClient.Index(
+ index,
+ &buf,
+ esClient.Index.WithDocumentID(idx),
+ esClient.Index.WithRefresh("true"),
+ )
+ if err != nil {
+ log.Logger.Error(err, "Error create response")
+ }
+ defer res.Body.Close()
+}
+
+func (e *esService) Insert(log string, processName string, using string, ts int64) {
+ doc := map[string]interface{}{
+ "log": log,
+ "name": processName,
+ "using": using,
+ "time": ts,
+ }
+ e.Index(config.CF.EsIndex, "", doc)
+}
+
+func (e *esService) Search(req model.GetLogReq) model.LogResp {
+ query := []func(*esapi.SearchRequest){
+ esClient.Search.WithIndex(config.CF.EsIndex),
+ esClient.Search.WithContext(context.Background()),
+ esClient.Search.WithPretty(),
+ esClient.Search.WithTrackTotalHits(true),
+ esClient.Search.WithFrom(req.Page.From),
+ esClient.Search.WithSize(req.Page.Size),
+ }
+ if req.Sort == "asc" {
+ query = append(query, esClient.Search.WithSort("time:asc"))
+ }
+ if req.Sort == "desc" {
+ query = append(query, esClient.Search.WithSort("time:desc"))
+ }
+ body := e.buildQueryBody(req)
+ var buf bytes.Buffer
+ if err := json.NewEncoder(&buf).Encode(body); err != nil {
+ log.Logger.Error(err)
+ return model.LogResp{}
+ }
+ query = append(query, esClient.Search.WithBody(&buf))
+ res, err := esClient.Search(query...)
+ if err != nil {
+ log.Logger.Error(err)
+ return model.LogResp{}
+ }
+ resp := model.EsResp{}
+ json.NewDecoder(res.Body).Decode(&resp)
+ res.Body.Close()
+ result := model.LogResp{}
+ for _, v := range resp.Hits.Hits {
+ result.Data = append(result.Data, model.Eslog{
+ Log: v.Source.Log,
+ Name: v.Source.Name,
+ Using: v.Source.Using,
+ Time: v.Source.Time,
+ Id: v.ID,
+ })
+ }
+ result.Total = resp.Hits.Total.Value
+ return result
+}
+
+func (e *esService) buildQueryBody(req model.GetLogReq) model.QueryBody {
+ result := model.QueryBody{}
+ if req.TimeRange.EndTime != 0 || req.TimeRange.StartTime != 0 {
+ result.Query.Bool.Must = append(result.Query.Bool.Must, map[string]any{
+ "range": map[string]any{
+ "time": map[string]any{
+ "gte": req.TimeRange.StartTime,
+ "lte": req.TimeRange.EndTime,
+ },
+ },
+ })
+ }
+ if req.Match.Log != "" {
+ result.Query.Bool.Must = append(result.Query.Bool.Must, map[string]any{
+ "match": map[string]any{
+ "log": req.Match.Log,
+ },
+ })
+ }
+ if req.Match.Name != "" {
+ result.Query.Bool.Must = append(result.Query.Bool.Must, map[string]any{
+ "match": map[string]any{
+ "name": req.Match.Name,
+ },
+ })
+ }
+ if req.Match.Using != "" {
+ result.Query.Bool.Must = append(result.Query.Bool.Must, map[string]any{
+ "match": map[string]any{
+ "using": req.Match.Using,
+ },
+ })
+ }
+ return result
+}
diff --git a/service/file/file.go b/service/file/file.go
new file mode 100644
index 0000000..955932b
--- /dev/null
+++ b/service/file/file.go
@@ -0,0 +1,76 @@
+package file
+
+import (
+ "fmt"
+ "io"
+ "msm/config"
+ "msm/log"
+ "msm/model"
+ "os"
+)
+
+type fileService struct{}
+
+var FileService = new(fileService)
+
+func (f *fileService) ReadFileFromPath(path string) (result []byte, err error) {
+ fi, err := os.Open(path)
+ if err != nil {
+ return
+ }
+ defer fi.Close()
+ fileInfo, err := fi.Stat()
+ if err != nil {
+ return
+ }
+ if size := float64(fileInfo.Size()) / 1e6; size > config.CF.FileSizeLimit {
+ err = fmt.Errorf("写入数据大小%vMB,超过%vMB限制", size, config.CF.FileSizeLimit)
+ return
+ }
+ result, err = io.ReadAll(fi)
+ if err != nil {
+ return
+ }
+ log.Logger.Debugw("文件写入成功", "path", path)
+ return
+}
+
+func (f *fileService) UpdateFileData(filePath string, file io.Reader, size int64) error {
+ if size := float64(size) / 1e6; size > config.CF.FileSizeLimit {
+ return fmt.Errorf("写入数据大小%vMB,超过%vMB限制", size, config.CF.FileSizeLimit)
+ }
+ fi, err := os.OpenFile(filePath, os.O_RDWR|os.O_TRUNC, 0777)
+ if err != nil {
+ return err
+ }
+ defer fi.Close()
+ if _, err = io.Copy(fi, file); err != nil {
+ return err
+ }
+ log.Logger.Debugw("文件写入成功", "path", filePath)
+ return nil
+}
+
+func (f *fileService) GetFileAndDirByPath(srcPath string) ([]model.FileStruct, error) {
+ result := []model.FileStruct{}
+ files, err := os.ReadDir(srcPath)
+ if err != nil {
+ return result, err
+ }
+ for _, file := range files {
+ result = append(result, model.FileStruct{
+ Name: file.Name(),
+ IsDir: file.IsDir(),
+ })
+ }
+ return result, nil
+}
+
+func (f *fileService) CreateNewDir(path string, name string) error {
+ _, err := os.Create(path + name)
+ return err
+}
+
+func (f *fileService) CreateNewFile(path string, name string) error {
+ return os.MkdirAll(path+name, os.ModeDir)
+}
diff --git a/service/log/loghandler.go b/service/log/loghandler.go
new file mode 100644
index 0000000..c084b49
--- /dev/null
+++ b/service/log/loghandler.go
@@ -0,0 +1,40 @@
+package loghandler
+
+import (
+ "msm/log"
+ "msm/model"
+ "msm/service/es"
+ "time"
+
+ "github.com/panjf2000/ants"
+)
+
+type loghandler struct{}
+
+var (
+ antsPool *ants.PoolWithFunc
+ Loghandler = new(loghandler)
+
+ logHanleFunc = func(i interface{}) {
+ esLog, ok := i.(model.Eslog)
+ if !ok {
+ log.Logger.Panicw("传入错误参数", "data", esLog)
+ return
+ }
+ es.EsService.Insert(esLog.Log, esLog.Name, esLog.Using, esLog.Time)
+ }
+
+ panicHanlderFunc = func(i interface{}) {
+ log.Logger.Error("es消息储存失败")
+ }
+)
+
+func init() {
+ antsPool, _ = ants.NewPoolWithFunc(1000, logHanleFunc, ants.WithPanicHandler(panicHanlderFunc), ants.WithExpiryDuration(time.Second*10))
+}
+
+func (l *loghandler) AddLog(data model.Eslog) {
+ if err := antsPool.Invoke(data); err != nil {
+ log.Logger.Errorw("协程池添加任务失败", "err", err, "当前运行数量", antsPool.Running())
+ }
+}
diff --git a/service/process/proccess.go b/service/process/proccess.go
new file mode 100644
index 0000000..225ee92
--- /dev/null
+++ b/service/process/proccess.go
@@ -0,0 +1,324 @@
+package process
+
+import (
+ "errors"
+ "msm/config"
+ "msm/log"
+ "msm/model"
+ loghandler "msm/service/log"
+ "msm/service/push"
+ "os/exec"
+ "strconv"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/gorilla/websocket"
+ pu "github.com/shirou/gopsutil/process"
+)
+
+type Process interface {
+ ReadCache(*websocket.Conn)
+ GetName() string
+ SetName(string)
+ GetTermType() string
+ SetTermType(string)
+ SetIsUsing(bool)
+ GetWhoUsing() string
+ SetWhoUsing(string)
+ SetStartCommand([]string)
+ GetControlController() string
+ SetControlController(string)
+ ChangControlChan() chan int
+ StopChan() chan struct{}
+ SetConfigLogReport(bool)
+ SetConfigStatuPush(bool)
+ SetConfigAutoRestart(bool)
+ GetStateInfo() string
+ GetStateState() uint8
+ Kill() error
+ SetWsConn(*websocket.Conn)
+ Write(string) error
+ WriteBytes([]byte) error
+ GetStartTimeFormat() string
+ VerifyControl() bool
+ ResetRestartTimes()
+ InitPerformanceStatus()
+ ProcessControl(string)
+ AddCpuUsage(float64)
+ AddMemUsage(float64)
+ AddRecordTime()
+ GetTimeRecord() []string
+ GetMemUsage() []float64
+ GetCpuUsage() []float64
+ monitorHanler()
+ initPsutil()
+ SetAutoRestart(bool)
+ TryLock() bool
+ Unlock()
+ ReStart()
+}
+
+type ProcessBase struct {
+ Name string
+ termType string
+ Pid int
+ cmd *exec.Cmd
+ IsUsing atomic.Bool
+ StartCommand []string
+ Lock sync.Mutex
+ WhoUsing string
+ stopChan chan struct{}
+ Control struct {
+ Controller string
+ changControlChan chan int
+ changControlTime time.Time
+ }
+ ws struct {
+ wsConnect *websocket.Conn
+ wsMux sync.RWMutex
+ }
+ Config struct {
+ AutoRestart bool
+ statuPush bool
+ logReport bool
+ }
+ State struct {
+ startTime time.Time
+ Info string
+ State uint8 //0 为未运行,1为运作中,2为异常状态
+ restartTimes int
+ }
+ performanceStatus struct {
+ cpu []float64
+ mem []float64
+ time []string
+ }
+ monitor struct {
+ enable bool
+ ProcessBase *pu.Process
+ }
+}
+
+func (p *ProcessBase) GetTermType() string {
+ return p.termType
+}
+
+func (p *ProcessBase) SetTermType(s string) {
+ p.termType = s
+}
+
+func (p *ProcessBase) GetStateInfo() string {
+ return p.State.Info
+}
+
+func (p *ProcessBase) GetStateState() uint8 {
+ return p.State.State
+}
+
+func (p *ProcessBase) SetAutoRestart(data bool) {
+ p.Config.AutoRestart = data
+}
+
+func (p *ProcessBase) GetWhoUsing() string {
+ return p.WhoUsing
+}
+
+func (p *ProcessBase) GetControlController() string {
+ return p.Control.Controller
+}
+
+func (p *ProcessBase) SetControlController(c string) {
+ p.Control.Controller = c
+}
+
+func (p *ProcessBase) SetWsConn(ws *websocket.Conn) {
+ p.ws.wsConnect = ws
+}
+
+func (p *ProcessBase) logReportHandler(log string) {
+ if config.CF.EsEnable && p.Config.logReport && len([]rune(log)) > config.CF.LogMinLenth {
+ loghandler.Loghandler.AddLog(model.Eslog{
+ Log: log,
+ Using: p.WhoUsing,
+ Name: p.Name,
+ Time: time.Now().UnixMilli(),
+ })
+ }
+}
+
+func (p *ProcessBase) GetStartTimeFormat() string {
+ return p.State.startTime.Format(time.DateTime)
+}
+
+func (p *ProcessBase) ProcessControl(name string) {
+ p.Control.changControlTime = time.Now()
+ p.Control.Controller = name
+ if p.State.State == 1 && p.IsUsing.Load() {
+ p.Control.changControlChan <- 0
+ }
+}
+
+// 没人在使用或控制时间过期
+func (p *ProcessBase) VerifyControl() bool {
+ return p.Control.Controller == "" || p.Control.changControlTime.Unix() < time.Now().Unix()-config.CF.ProcessExpireTime
+}
+
+func (p *ProcessBase) setProcessConfig(pconfig model.Process) {
+ p.Config.AutoRestart = pconfig.AutoRestart
+ p.Config.logReport = pconfig.LogReport
+ p.Config.statuPush = pconfig.Push
+}
+
+func (p *ProcessBase) ResetRestartTimes() {
+ p.State.restartTimes = 0
+}
+
+func (p *ProcessBase) push(message string) {
+ if p.Config.statuPush {
+ messagePlaceholders := map[string]string{
+ "{$name}": p.Name,
+ "{$user}": p.WhoUsing,
+ "{$message}": message,
+ "{$status}": strconv.Itoa(int(p.State.State)),
+ }
+ push.PushService.Push(messagePlaceholders)
+ }
+}
+
+func (p *ProcessBase) InitPerformanceStatus() {
+ p.performanceStatus.cpu = make([]float64, config.CF.PerformanceInfoListLength)
+ p.performanceStatus.mem = make([]float64, config.CF.PerformanceInfoListLength)
+ p.performanceStatus.time = make([]string, config.CF.PerformanceInfoListLength)
+}
+
+func (p *ProcessBase) AddCpuUsage(usage float64) {
+ p.performanceStatus.cpu = append(p.performanceStatus.cpu[1:], usage)
+}
+
+func (p *ProcessBase) AddMemUsage(usage float64) {
+ p.performanceStatus.mem = append(p.performanceStatus.mem[1:], usage)
+}
+
+func (p *ProcessBase) AddRecordTime() {
+ p.performanceStatus.time = append(p.performanceStatus.time[1:], time.Now().Format(time.DateTime))
+}
+func (p *ProcessBase) GetCpuUsage() []float64 {
+ return p.performanceStatus.cpu
+}
+
+func (p *ProcessBase) GetMemUsage() []float64 {
+ return p.performanceStatus.mem
+}
+
+func (p *ProcessBase) GetTimeRecord() []string {
+ return p.performanceStatus.time
+}
+
+func (p *ProcessBase) monitorHanler() {
+ defer log.Logger.Infow("性能监控结束", "name", p.Name, "pid", p.Pid)
+ for {
+ if !p.monitor.enable {
+ return
+ }
+ select {
+ case <-time.After(time.Minute * time.Duration(config.CF.PerformanceInfoInterval)):
+ if p.State.State != 1 {
+ log.Logger.Debugw("进程状态异常,跳过监控数据获取", "name", p.Name)
+ p.AddCpuUsage(0)
+ p.AddMemUsage(0)
+ p.AddRecordTime()
+ continue
+ }
+ ProcessBase := p.monitor.ProcessBase
+ cpuPercent, err := ProcessBase.CPUPercent()
+ if err != nil {
+ log.Logger.Errorw("CPU使用率获取失败", "err", err)
+ return
+ }
+ memInfo, err := ProcessBase.MemoryInfo()
+ if err != nil {
+ log.Logger.Errorw("内存使用率获取失败", "err", err)
+ return
+ }
+ p.AddRecordTime()
+ p.AddCpuUsage(cpuPercent)
+ p.AddMemUsage(float64(memInfo.RSS / 1000))
+ log.Logger.Debugw("进程资源使用率获取成功", "pid", p.Pid, "name", p.Name, "cpu", cpuPercent, "mem", memInfo.RSS)
+ case <-p.stopChan:
+ return
+ }
+ }
+}
+
+func (p *ProcessBase) initPsutil() {
+ pup, err := pu.NewProcess(int32(p.Pid))
+ if err != nil {
+ p.monitor.enable = false
+ log.Logger.Debug("pu进程获取失败")
+ } else {
+ p.monitor.enable = true
+ log.Logger.Debug("pu进程获取成功")
+ p.monitor.ProcessBase = pup
+ }
+}
+
+func (p *ProcessBase) SetConfigLogReport(b bool) {
+ p.Config.logReport = b
+}
+
+func (p *ProcessBase) SetConfigAutoRestart(b bool) {
+ p.Config.AutoRestart = b
+}
+
+func (p *ProcessBase) SetConfigStatuPush(b bool) {
+ p.Config.statuPush = b
+}
+
+func (p *ProcessBase) SetName(s string) {
+ p.Name = s
+}
+
+func (p *ProcessBase) SetStartCommand(cmd []string) {
+ p.StartCommand = cmd
+}
+
+func (p *ProcessBase) ChangControlChan() chan int {
+ return p.Control.changControlChan
+}
+
+func (p *ProcessBase) SetIsUsing(b bool) {
+ p.IsUsing.Store(b)
+}
+
+func (p *ProcessBase) GetName() string {
+ return p.Name
+}
+
+func (p *ProcessBase) SetWhoUsing(s string) {
+ p.WhoUsing = s
+}
+
+func (p *ProcessBase) StopChan() chan struct{} {
+ return p.stopChan
+}
+
+func (p *ProcessBase) TryLock() bool {
+ return p.Lock.TryLock()
+}
+
+func (p *ProcessBase) Unlock() {
+ p.Lock.Unlock()
+}
+
+func RunNewProcess(config model.Process) (proc Process, err error) {
+ switch config.TermType {
+ case "std":
+ proc, err = RunNewProcessStd(config)
+ case "pty":
+ proc, err = RunNewProcessPty(config)
+ default:
+ err = errors.New("终端类型错误")
+ }
+ return
+}
diff --git a/service/process/process_pty.go b/service/process/process_pty.go
new file mode 100644
index 0000000..d8094b4
--- /dev/null
+++ b/service/process/process_pty.go
@@ -0,0 +1,169 @@
+package process
+
+import (
+ "bytes"
+ "fmt"
+ "msm/config"
+ "msm/log"
+ "msm/model"
+ "msm/utils"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "github.com/creack/pty"
+ "github.com/gorilla/websocket"
+)
+
+type ProcessPty struct {
+ ProcessBase
+ cacheBytesBuf *bytes.Buffer
+ pty *os.File
+}
+
+func (p *ProcessPty) Kill() error {
+ if err := p.cmd.Process.Kill(); err != nil {
+ log.Logger.Errorw("进程杀死失败", "err", err, "state", p.State.State)
+ return err
+ }
+ return p.pty.Close()
+}
+
+func (p *ProcessPty) watchDog() {
+ state, _ := p.cmd.Process.Wait()
+ close(p.stopChan)
+ p.State.State = 0
+ p.pty.Close()
+ if state.ExitCode() != 0 {
+ log.Logger.Infow("进程停止", "进程名称", p.Name, "exitCode", state.ExitCode(), "进程类型", "pty")
+ p.push(fmt.Sprintf("进程停止,退出码 %d", state.ExitCode()))
+ if p.Config.AutoRestart {
+ p.ReStart()
+ }
+ } else {
+ log.Logger.Infow("进程正常退出", "进程名称", p.Name)
+ p.push("进程正常退出")
+ }
+}
+
+func (p *ProcessPty) ReStart() {
+ if p.State.restartTimes > config.CF.ProcessRestartsLimit {
+ log.Logger.Warnw("重启次数达到上限", "name", p.Name, "limit", config.CF.ProcessRestartsLimit)
+ p.State.State = 2
+ p.State.Info = "重启次数异常"
+ p.push("进程重启次数达到上限")
+ return
+ }
+ cmd := exec.Command(p.StartCommand[0], p.StartCommand[1:]...)
+ cmd.Dir = p.cmd.Dir
+ pf, err := pty.Start(cmd)
+ if err != nil || p.cmd.Process == nil {
+ log.Logger.Error("进程启动出错:", err)
+ return
+ }
+ pty.Setsize(pf, &pty.Winsize{
+ Rows: 100,
+ Cols: 100,
+ })
+ p.pty = pf
+ p.State.restartTimes++
+ log.Logger.Infow("进程启动成功", "进程名称", p.Name, "重启次数", p.State.restartTimes)
+ p.cmd = cmd
+ p.pInit()
+ p.push("进程启动成功")
+}
+
+func (p *ProcessPty) WriteBytes(input []byte) (err error) {
+ p.logReportHandler(config.CF.ProcessInputPrefix + string(input))
+ _, err = p.pty.Write(input)
+ return
+}
+
+func (p *ProcessPty) Write(input string) (err error) {
+ p.logReportHandler(config.CF.ProcessInputPrefix + input)
+ _, err = p.pty.Write([]byte(input))
+ return
+}
+
+func (p *ProcessPty) readInit() {
+ log.Logger.Debugw("stdout读取线程已启动", "进程名", p.Name, "使用者", p.WhoUsing)
+ buf := make([]byte, 1024)
+ for {
+ select {
+ case <-p.stopChan:
+ {
+ p.IsUsing.Store(false)
+ p.WhoUsing = ""
+ log.Logger.Debugw("stdout读取线程已退出", "进程名", p.Name, "使用者", p.WhoUsing)
+ return
+ }
+ default:
+ {
+ n, _ := p.pty.Read(buf)
+ p.bufHanle(buf[:n])
+ if p.IsUsing.Load() {
+ p.ws.wsMux.Lock()
+ p.ws.wsConnect.WriteMessage(websocket.TextMessage, buf[:n])
+ p.ws.wsMux.Unlock()
+ }
+ }
+ }
+ }
+}
+
+func (p *ProcessPty) ReadCache(ws *websocket.Conn) {
+ ws.WriteMessage(websocket.TextMessage, p.cacheBytesBuf.Bytes())
+}
+
+func (p *ProcessPty) bufHanle(b []byte) {
+ log := strings.TrimSpace(string(b))
+ if utils.RemoveANSI(log) != "" {
+ p.logReportHandler(log)
+ }
+ p.cacheBytesBuf.Write(b)
+ p.cacheBytesBuf.Next(len(b))
+}
+
+func (p *ProcessPty) pInit() {
+ p.SetTermType("pty")
+ p.Control.changControlChan = make(chan int)
+ p.stopChan = make(chan struct{})
+ p.State.State = 1
+ p.Pid = p.cmd.Process.Pid
+ p.State.startTime = time.Now()
+ p.cacheBytesBuf = bytes.NewBuffer(make([]byte, config.CF.ProcessMsgCacheBufLimit))
+ p.InitPerformanceStatus()
+ p.initPsutil()
+ go p.readInit()
+ go p.monitorHanler()
+ go p.watchDog()
+}
+
+func RunNewProcessPty(pconfig model.Process) (*ProcessPty, error) {
+ args := strings.Split(pconfig.Cmd, " ")
+ cmd := exec.Command(args[0], args[1:]...) // 替换为你要执行的命令及参数
+
+ processPty := ProcessPty{
+ ProcessBase: ProcessBase{
+ Name: pconfig.Name,
+ StartCommand: args,
+ },
+ }
+ cmd.Dir = pconfig.Cwd
+ pf, err := pty.Start(cmd)
+ if err != nil || cmd.Process == nil {
+ log.Logger.Error("进程启动出错:", err)
+ return nil, err
+ }
+ pty.Setsize(pf, &pty.Winsize{
+ Rows: 100,
+ Cols: 100,
+ })
+ processPty.pty = pf
+ processPty.cmd = cmd
+ log.Logger.Infow("创建进程成功")
+ processPty.setProcessConfig(pconfig)
+ processPty.pInit()
+ return &processPty, nil
+}
diff --git a/service/process/process_std.go b/service/process/process_std.go
new file mode 100644
index 0000000..c1f6a51
--- /dev/null
+++ b/service/process/process_std.go
@@ -0,0 +1,182 @@
+package process
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "msm/config"
+ "msm/log"
+ "msm/model"
+ "os/exec"
+ "strings"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+type ProcessStd struct {
+ ProcessBase
+ cacheLine []string
+ stdin io.WriteCloser
+ stdout *bufio.Scanner
+}
+
+func (p *ProcessStd) Kill() error {
+ return p.cmd.Process.Kill()
+}
+
+func (p *ProcessStd) watchDog() {
+ state, _ := p.cmd.Process.Wait()
+ close(p.stopChan)
+ p.State.State = 0
+ if state.ExitCode() != 0 {
+ log.Logger.Infow("进程停止", "进程名称", p.Name, "exitCode", state.ExitCode(), "进程类型", "std")
+ p.push(fmt.Sprintf("进程停止,退出码 %d", state.ExitCode()))
+ if p.Config.AutoRestart {
+ p.ReStart()
+ }
+ } else {
+ log.Logger.Infow("进程正常退出", "进程名称", p.Name)
+ p.push("进程正常退出")
+ }
+}
+
+func (p *ProcessStd) WriteBytes(input []byte) (err error) {
+ p.logReportHandler(config.CF.ProcessInputPrefix + string(input))
+ _, err = p.stdin.Write(append(input, '\n'))
+ return
+}
+
+func (p *ProcessStd) Write(input string) (err error) {
+ p.logReportHandler(config.CF.ProcessInputPrefix + input)
+ _, err = p.stdin.Write([]byte(input + "\n"))
+ return
+}
+
+func (p *ProcessStd) ReStart() {
+ if p.State.restartTimes > config.CF.ProcessRestartsLimit {
+ log.Logger.Warnw("重启次数达到上限", "name", p.Name, "limit", config.CF.ProcessRestartsLimit)
+ p.State.State = 2
+ p.State.Info = "重启次数异常"
+ p.push("进程重启次数达到上限")
+ return
+ }
+ cmd := exec.Command(p.StartCommand[0], p.StartCommand[1:]...) // 替换为你要执行的命令及参数
+ cmd.Dir = p.cmd.Dir
+ out, err := cmd.StdoutPipe()
+ if err != nil {
+ log.Logger.Errorw("重启失败,输出管道获取失败", "err", err)
+ p.Config.AutoRestart = false
+ return
+ }
+ p.stdout = bufio.NewScanner(out)
+ p.stdin, err = cmd.StdinPipe()
+ if err != nil {
+ log.Logger.Errorw("重启失败,输入管道获取失败", "err", err)
+ p.Config.AutoRestart = false
+ return
+ }
+ err = cmd.Start()
+ if err != nil {
+ log.Logger.Errorw("重启失败,进程启动出错:", "err", err)
+ p.Config.AutoRestart = false
+ return
+ }
+ p.State.restartTimes++
+ log.Logger.Infow("进程启动成功", "进程名称", p.Name, "重启次数", p.State.restartTimes)
+ p.cmd = cmd
+ p.pInit()
+ p.push("进程启动成功")
+
+}
+
+func (p *ProcessStd) pInit() {
+ log.Logger.Infow("创建进程成功")
+ p.Control.changControlChan = make(chan int)
+ p.stopChan = make(chan struct{})
+ p.State.State = 1
+ p.Pid = p.cmd.Process.Pid
+ p.State.startTime = time.Now()
+ p.cacheLine = make([]string, config.CF.ProcessMsgCacheLinesLimit)
+ p.InitPerformanceStatus()
+ p.initPsutil()
+ go p.watchDog()
+ go p.readInit()
+ go p.monitorHanler()
+}
+
+func (p *ProcessStd) ReadCache(ws *websocket.Conn) {
+ for _, line := range p.cacheLine {
+ ws.WriteMessage(websocket.TextMessage, []byte(line))
+ }
+}
+
+func (p *ProcessStd) readInit() {
+ var output string
+ log.Logger.Debugw("stdout读取线程已启动", "进程名", p.Name, "使用者", p.WhoUsing)
+ for {
+ select {
+ case <-p.stopChan:
+ {
+ p.IsUsing.Store(false)
+ p.WhoUsing = ""
+ log.Logger.Debugw("stdout读取线程已退出", "进程名", p.Name, "使用者", p.WhoUsing)
+ return
+ }
+ default:
+ {
+ output = p.Read()
+ if p.IsUsing.Load() && output != "" {
+ p.ws.wsMux.Lock()
+ p.ws.wsConnect.WriteMessage(websocket.TextMessage, []byte(output))
+ p.ws.wsMux.Unlock()
+ }
+ }
+ }
+ }
+}
+func (p *ProcessStd) Read() string {
+ if p.stdout.Scan() {
+ output := p.stdout.Text()
+ p.logReportHandler(output)
+ p.cacheLine = p.cacheLine[1:]
+ p.cacheLine = append(p.cacheLine, output)
+ return output
+ }
+ return ""
+}
+
+func RunNewProcessStd(pconfig model.Process) (*ProcessStd, error) {
+ args := strings.Split(pconfig.Cmd, " ")
+ cmd := exec.Command(args[0], args[1:]...) // 替换为你要执行的命令及参数
+
+ processStd := ProcessStd{
+ ProcessBase: ProcessBase{
+ Name: pconfig.Name,
+ StartCommand: args,
+ },
+ }
+ cmd.Dir = pconfig.Cwd
+ out, err := cmd.StdoutPipe()
+ if err != nil {
+ log.Logger.Errorw("输出管道获取失败", "err", err)
+ return nil, err
+ }
+ processStd.stdout = bufio.NewScanner(out)
+ processStd.stdin, err = cmd.StdinPipe()
+ if err != nil {
+ log.Logger.Errorw("输入管道获取失败", "err", err)
+ return nil, err
+ }
+ err = cmd.Start()
+ if err != nil || cmd.Process == nil {
+ log.Logger.Error("进程启动出错:", err)
+ return nil, err
+ }
+ log.Logger.Infow("创建进程成功", "config", pconfig)
+ processStd.cmd = cmd
+ processStd.SetTermType("std")
+ processStd.pInit()
+ processStd.setProcessConfig(pconfig)
+ return &processStd, nil
+}
diff --git a/service/process/service.go b/service/process/service.go
new file mode 100644
index 0000000..86c48d2
--- /dev/null
+++ b/service/process/service.go
@@ -0,0 +1,124 @@
+package process
+
+import (
+ "errors"
+ "msm/dao"
+ "msm/log"
+ "msm/model"
+ "strings"
+ "sync"
+)
+
+type processCtlService struct{}
+
+var processMap sync.Map = sync.Map{}
+var ProcessCtlService = new(processCtlService)
+
+func (p *processCtlService) AddProcess(uuid int, prcess Process) {
+ processMap.Store(uuid, prcess)
+ // processMap.Store("111", prcess)
+ // return "111"
+}
+
+func (p *processCtlService) KillProcess(uuid int) error {
+ value, ok := processMap.Load(uuid)
+ if !ok {
+ return errors.New("进程不存在")
+ }
+ result, ok := value.(Process)
+ if !ok {
+ return errors.New("进程类型错误")
+ }
+ result.SetAutoRestart(false)
+ return result.Kill()
+}
+
+func (p *processCtlService) GetProcess(uuid int) (Process, error) {
+ process, ok := processMap.Load(uuid)
+ if !ok {
+ return nil, errors.New("进程获取失败")
+
+ }
+ result, ok := process.(Process)
+ if !ok {
+ return nil, errors.New("进程类型错误")
+
+ }
+ return result, nil
+}
+
+func (p *processCtlService) KillAllProcess() {
+ processMap.Range(func(key, value any) bool {
+ value.(Process).Kill()
+ return true
+ })
+}
+
+func (p *processCtlService) DeleteProcess(uuid int) {
+ processMap.Delete(uuid)
+}
+
+func (p *processCtlService) GetProcessList() []model.ProcessInfo {
+ processConfiglist := dao.ProcessDao.GetAllProcessConfig()
+ return p.getProcessInfoList(processConfiglist)
+}
+
+func (p *processCtlService) GetProcessListByUser(username string) []model.ProcessInfo {
+ processConfiglist := dao.ProcessDao.GetProcessConfigByUser(username)
+ return p.getProcessInfoList(processConfiglist)
+}
+
+func (p *processCtlService) getProcessInfoList(processConfiglist []model.Process) []model.ProcessInfo {
+ processInfoList := []model.ProcessInfo{}
+ for _, v := range processConfiglist {
+ pi := model.ProcessInfo{
+ Name: v.Name,
+ Uuid: v.Uuid,
+ }
+ if value, ok := processMap.Load(v.Uuid); ok {
+ process := value.(Process)
+ pi.State.Info = process.GetStateInfo()
+ pi.State.State = process.GetStateState()
+ pi.StartTime = process.GetStartTimeFormat()
+ pi.User = process.GetWhoUsing()
+ pi.Usage.Cpu = process.GetCpuUsage()
+ pi.Usage.Mem = process.GetMemUsage()
+ pi.Usage.Time = process.GetTimeRecord()
+ pi.TermType = process.GetTermType()
+ }
+ processInfoList = append(processInfoList, pi)
+ }
+ return processInfoList
+}
+
+func (p *processCtlService) ProcessInit() {
+ config := dao.ProcessDao.GetAllProcessConfig()
+ for _, v := range config {
+ if !v.AutoRestart {
+ continue
+ }
+ proc, err := RunNewProcess(v)
+ if err != nil {
+ log.Logger.Warnw("初始化启动进程失败", v.Name, "name", "err", err)
+ continue
+ }
+ p.AddProcess(v.Uuid, proc)
+ }
+}
+
+func (p *processCtlService) UpdateProcessConfig(config model.Process) error {
+ process, ok := processMap.Load(config.Uuid)
+ if !ok {
+ return errors.New("进程获取失败")
+ }
+ result, ok := process.(Process)
+ if !ok {
+ return errors.New("进程类型错误")
+ }
+ result.SetConfigLogReport(config.LogReport)
+ result.SetConfigStatuPush(config.Push)
+ result.SetConfigAutoRestart(config.AutoRestart)
+ result.SetStartCommand(strings.Split(config.Cmd, " "))
+ result.SetName(config.Name)
+ return nil
+}
diff --git a/service/push/push.go b/service/push/push.go
new file mode 100644
index 0000000..bb0e55d
--- /dev/null
+++ b/service/push/push.go
@@ -0,0 +1,35 @@
+package push
+
+import (
+ "msm/dao"
+ "strings"
+
+ "github.com/levigross/grequests"
+)
+
+type pushService struct{}
+
+var PushService = new(pushService)
+
+func (p *pushService) Push(placeholders map[string]string) {
+ pl := dao.PushDao.GetPushList()
+ for _, v := range pl {
+ if v.Enable {
+ if v.Method == "GET" {
+ grequests.Get(p.getReplaceMessage(placeholders, v.Url), nil)
+ }
+ if v.Method == "POST" {
+ grequests.Post(v.Url, &grequests.RequestOptions{
+ JSON: p.getReplaceMessage(placeholders, v.Body),
+ })
+ }
+ }
+ }
+}
+
+func (p *pushService) getReplaceMessage(placeholders map[string]string, message string) string {
+ for k, v := range placeholders {
+ message = strings.ReplaceAll(message, k, v)
+ }
+ return message
+}
diff --git a/termui/tui.go b/termui/tui.go
new file mode 100644
index 0000000..b490ca7
--- /dev/null
+++ b/termui/tui.go
@@ -0,0 +1,58 @@
+package termui
+
+// func TermuiInit() {
+// // time.Sleep(2 * time.Second)
+// err := termbox.Init()
+// if err != nil {
+// log.Logger.Errorw("termui初始化失败", "err", err)
+// return
+// }
+// homeUi()
+// }
+
+// func homeUi() {
+// termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
+// termbox.Flush()
+// list := process.ProcessCtlService.GetProcessList()
+// fmt.Println()
+// for i, v := range list {
+// if v.User != "" {
+// fmt.Printf(" [%v] %v %v <%v>\n", i, v.Name, v.StartTime, v.User)
+// } else {
+// fmt.Printf(" [%v] %v %v\n", i, v.Name, v.StartTime)
+// }
+// }
+// input := ""
+// fmt.Scan(&input)
+// for i, v := range list {
+// if input == strconv.Itoa(i) {
+// // prcessUi(v.Uuid)
+// }
+// }
+// }
+
+// func prcessUi(uuid int) {
+// termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
+// termbox.Flush()
+// proc, err := process.ProcessCtlService.GetProcess(uuid)
+// if err != nil {
+// log.Logger.Errorw("进程获取失败", "err", err)
+// return
+// }
+// proc.SetControl("")
+// go func() {
+// for {
+// if output := proc.Read(); output != "" {
+// fmt.Println(output)
+// }
+// }
+// }()
+// go func() {
+// input := ""
+// for {
+// fmt.Scan(&input)
+// proc.Write(input + "\n")
+// }
+// }()
+
+// }
diff --git a/test/test.go b/test/test.go
new file mode 100644
index 0000000..78bf89b
--- /dev/null
+++ b/test/test.go
@@ -0,0 +1,111 @@
+package main
+
+import (
+ "io"
+ "log"
+ "net/http"
+ "os/exec"
+
+ "github.com/creack/pty"
+ "github.com/gorilla/websocket"
+)
+
+var upgrader = websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool {
+ return true
+ },
+}
+
+func main() {
+ http.HandleFunc("/ws", handleWebSocket)
+ log.Println("Server started at :8080")
+ log.Fatal(http.ListenAndServe(":8080", nil))
+}
+
+func handleWebSocket(w http.ResponseWriter, r *http.Request) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Println("Upgrade:", err)
+ return
+ }
+ defer conn.Close()
+
+ // 启动 Minecraft 服务器
+ cmd := exec.Command("java", "-jar", "launcher-airplane.jar")
+
+ // 创建伪终端
+ ptmx, err := pty.Start(cmd)
+ if err != nil {
+ log.Println("Start pty:", err)
+ return
+ }
+ defer func() { _ = ptmx.Close() }() // 最后关闭伪终端
+
+ // 创建通道用于读取子程序的输出
+ outputChan := make(chan string)
+
+ go func() {
+ defer close(outputChan)
+ readOutput(ptmx, outputChan)
+ }()
+
+ // 将子程序的输出发送到 WebSocket 客户端
+ go func() {
+ for output := range outputChan {
+ if err := conn.WriteMessage(websocket.TextMessage, []byte(output)); err != nil {
+ log.Println("WriteMessage:", err)
+ break
+ }
+ }
+ }()
+
+ // 从 WebSocket 客户端读取消息并发送到子程序的标准输入
+ for {
+ _, message, err := conn.ReadMessage()
+ if err != nil {
+ log.Println("ReadMessage:", err)
+ break
+ }
+ // 检查是否是发送 Tab 键的命令
+ if string(message) == "SEND_TAB" {
+ _, err := ptmx.Write([]byte{9}) // Tab 键的 ASCII 码是 9
+ if err != nil {
+ log.Println("Write to stdin:", err)
+ }
+ } else if string(message) == "READ_INPUT" {
+ // 读取伪终端当前输入管道内已经存在的内容
+ // 注意:伪终端没有单独的输入缓冲区,输入会立即被处理
+ // 这里假设你想获取当前输出内容
+ if err := conn.WriteMessage(websocket.TextMessage, []byte("Currently no direct way to fetch unsent input. Consider monitoring the terminal buffer.")); err != nil {
+ log.Println("WriteMessage:", err)
+ }
+ } else {
+ _, err := ptmx.Write(append(message, '\n')) // 确保命令后有换行符
+ if err != nil {
+ log.Println("Write to stdin:", err)
+ }
+ }
+ }
+
+ // 等待子程序结束
+ if err := cmd.Wait(); err != nil {
+ log.Println("Wait:", err)
+ return
+ }
+}
+
+func readOutput(reader io.Reader, outputChan chan<- string) {
+ buf := make([]byte, 1024)
+ for {
+ n, err := reader.Read(buf)
+ if n > 0 {
+ outputChan <- string(buf[:n])
+ }
+ if err != nil {
+ if err != io.EOF {
+ log.Println("Read:", err)
+ }
+ break
+ }
+ }
+}
diff --git a/utils/jwt.go b/utils/jwt.go
new file mode 100644
index 0000000..4e06f9f
--- /dev/null
+++ b/utils/jwt.go
@@ -0,0 +1,74 @@
+package utils
+
+import (
+ "errors"
+ "time"
+
+ "github.com/golang-jwt/jwt"
+)
+
+var mySecret = []byte("D9&(#$HI$#Y(FD*A))")
+
+func keyFunc(_ *jwt.Token) (i interface{}, err error) {
+ return mySecret, nil
+}
+
+// MyClaims 自定义声明结构体并内嵌 jwt.StandardClaims
+// jwt包自带的jwt.StandardClaims只包含了官方字段,若需要额外记录其他字段,就可以自定义结构体
+// 如果想要保存更多信息,都可以添加到这个结构体中
+
+type MyClaims struct {
+ UserName string `json:"user_name"`
+ jwt.StandardClaims
+}
+
+func GenToken(UserName string) (string, error) {
+ // 创建一个我们自己的声明的数据
+ c := MyClaims{
+ UserName,
+ jwt.StandardClaims{
+ ExpiresAt: time.Now().Add(
+ 240 * time.Hour).Unix(), // 过期时间
+ Issuer: "jwt", // 签发人
+ },
+ }
+ // 使用指定的签名方法创建签名对象
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
+ // 使用指定的secret签名并获得完整的编码后的字符串token
+ return token.SignedString(mySecret)
+}
+
+// ParseToken 解析JWT
+func ParseToken(tokenString string) (*MyClaims, error) {
+ // 解析token
+ var mc = new(MyClaims)
+ token, err := jwt.ParseWithClaims(tokenString, mc, keyFunc)
+ if err != nil {
+ return nil, err
+ }
+ // 校验token
+ if token.Valid {
+ return mc, nil
+ }
+ return nil, errors.New("invalid token")
+}
+
+// RefreshToken 刷新AccessToken
+func RefreshToken(aToken, rToken string) (newAToken, newRToken string, err error) {
+ // refresh token无效直接返回
+ if _, err = jwt.Parse(rToken, keyFunc); err != nil {
+ return
+ }
+
+ // 从旧access token中解析出claims数据
+ var claims MyClaims
+ _, err = jwt.ParseWithClaims(aToken, &claims, keyFunc)
+ v, _ := err.(*jwt.ValidationError)
+
+ // 当access token是过期错误 并且 refresh token没有过期时就创建一个新的access token
+ if v.Errors == jwt.ValidationErrorExpired {
+ token, _ := GenToken(claims.UserName)
+ return token, "", nil
+ }
+ return
+}
diff --git a/utils/md5.go b/utils/md5.go
new file mode 100644
index 0000000..52c108d
--- /dev/null
+++ b/utils/md5.go
@@ -0,0 +1,12 @@
+package utils
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+)
+
+func Md5(str string) string {
+ h := md5.New()
+ h.Write([]byte(str))
+ return hex.EncodeToString(h.Sum(nil))
+}
diff --git a/utils/unicode.go b/utils/unicode.go
new file mode 100644
index 0000000..5d37795
--- /dev/null
+++ b/utils/unicode.go
@@ -0,0 +1,32 @@
+package utils
+
+import (
+ "regexp"
+ "unicode/utf8"
+)
+
+func RemoveNotValidUtf8InString(s string) string {
+ ret := s
+ if !utf8.ValidString(s) {
+ v := make([]rune, 0, len(s))
+ for i, r := range s {
+ if r == utf8.RuneError {
+ _, size := utf8.DecodeRuneInString(s[i:])
+ if size == 1 {
+ continue
+ }
+ }
+ v = append(v, r)
+ }
+ ret = string(v)
+ }
+ return ret
+}
+
+func RemoveANSI(input string) string {
+ // Define the regular expression to match ANSI escape sequences
+ re := regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
+ // Replace all ANSI escape sequences with an empty string
+ cleanedString := re.ReplaceAllString(input, "")
+ return cleanedString
+}
diff --git a/utils/utils.go b/utils/utils.go
new file mode 100644
index 0000000..a052760
--- /dev/null
+++ b/utils/utils.go
@@ -0,0 +1,8 @@
+package utils
+
+func Unwarp[T any](result T, err error) T {
+ if err != nil {
+ panic(err)
+ }
+ return result
+}