# HG changeset patch # User Sebastien Jodogne # Date 1585244821 -3600 # Node ID 7ed502b17b8f54de8885d96a413c9679f694ad4b initial commit diff -r 000000000000 -r 7ed502b17b8f AUTHORS --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/AUTHORS Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,12 @@ +Python plugin for Orthanc +========================= + + +Authors +------- + +* Osimis S.A. + Quai Banning 6 + 4000 Liege + Belgium + http://www.osimis.io/ diff -r 000000000000 -r 7ed502b17b8f CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CMakeLists.txt Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,94 @@ +cmake_minimum_required(VERSION 2.8) +project(OrthancPython) + +set(PLUGIN_VERSION "mainline") + + +set(PYTHON_VERSION "3.6" CACHE STRING "Version of Python to be used") +set(PYTHON_WINDOWS_ROOT "" CACHE STRING "") +set(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost") +set(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") +set(USE_LEGACY_JSONCPP OFF CACHE BOOL "Use the old branch 0.x.y of JsonCpp, that does not require a C++11 compiler (for LSB and old versions of Visual Studio)") +set(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK") + + +include(CheckIncludeFile) +include(CheckIncludeFileCXX) +include(CheckIncludeFiles) +include(CheckLibraryExists) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + set(PYTHON_INCLUDE_DIRS ${PYTHON_WINDOWS_ROOT}/include) + set(PYTHON_LIBRARIES ${PYTHON_WINDOWS_ROOT}/libs/libpython27.a) + +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(PYTHON_1 python-${PYTHON_VERSION}-embed) + + if (PYTHON_1_FOUND) + set(PYTHON_INCLUDE_DIRS ${PYTHON_1_INCLUDE_DIRS}) + set(PYTHON_LIBRARIES ${PYTHON_1_LIBRARIES}) + else() + pkg_check_modules(PYTHON_2 REQUIRED python-${PYTHON_VERSION}) + set(PYTHON_INCLUDE_DIRS ${PYTHON_2_INCLUDE_DIRS}) + set(PYTHON_LIBRARIES ${PYTHON_2_LIBRARIES}) + endif() +endif() + +set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/Resources/Orthanc) +include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake) + +if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK) + include_directories( + ${ORTHANC_ROOT}/Sdk-1.5.7 + ) +else() + CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCPlugin.h HAVE_ORTHANC_H) + if (NOT HAVE_ORTHANC_H) + message(FATAL_ERROR "Please install the headers of the Orthanc plugins SDK") + endif() +endif() + +add_definitions( + -DHAS_ORTHANC_EXCEPTION=0 + ) + +include_directories( + ${ORTHANC_ROOT}/Plugins/Include + ${ORTHANC_ROOT}/Plugins/Samples/Common + ${PYTHON_INCLUDE_DIRS} + ) + +add_library(OrthancPython SHARED + ${BOOST_SOURCES} + ${JSONCPP_SOURCES} + ${ORTHANC_ROOT}/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp + Sources/Autogenerated/sdk.cpp + Sources/OnChangeCallback.cpp + Sources/OnStoredInstanceCallback.cpp + Sources/Plugin.cpp + Sources/PythonFunction.cpp + Sources/PythonLock.cpp + Sources/PythonModule.cpp + Sources/PythonObject.cpp + Sources/PythonString.cpp + Sources/RestCallbacks.cpp + ) + +target_link_libraries(OrthancPython ${PYTHON_LIBRARIES}) + +add_definitions(-DPLUGIN_VERSION="${PLUGIN_VERSION}") + +set_target_properties(OrthancPython PROPERTIES + VERSION ${PLUGIN_VERSION} + SOVERSION ${PLUGIN_VERSION} + ) + +install( + TARGETS OrthancPython + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) diff -r 000000000000 -r 7ed502b17b8f COPYING --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/COPYING Thu Mar 26 18:47:01 2020 +0100 @@ -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 -r 000000000000 -r 7ed502b17b8f CodeAnalysis/Class.mustache --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CodeAnalysis/Class.mustache Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,163 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +typedef struct +{ + PyObject_HEAD + + /* Type-specific fields go here. */ + {{class_name}}* object_; + bool borrowed_; +} sdk_{{class_name}}_Object; + + + +// Forward declaration of the methods +{{#methods}} +static PyObject *sdk_{{class_name}}_{{c_function}}( + sdk_{{class_name}}_Object* self, PyObject *args); +{{/methods}} + + +static PyMethodDef sdk_{{class_name}}_Methods[] = { +{{#methods}} + { "{{short_name}}", + (PyCFunction) sdk_{{class_name}}_{{c_function}}, METH_VARARGS, + "Generated from C function {{c_function}}()" }, +{{/methods}} + { NULL } /* Sentinel */ +}; + + +static int sdk_{{class_name}}_Constructor( + sdk_{{class_name}}_Object *self, PyObject *args, PyObject *kwds) +{ + OrthancPlugins::LogInfo("Creating Python object of class {{class_name}}"); + + self->object_ = NULL; + self->borrowed_ = false; + + long long object = 0; + unsigned char borrowed = false; + + if (PyArg_ParseTuple(args, "Lb", &object, &borrowed)) + { + self->object_ = reinterpret_cast<{{class_name}}*>(static_cast(object)); + self->borrowed_ = borrowed; + return 0; + } + else + { + PyErr_SetString(PyExc_ValueError, "Expected a pair (pointer, borrowed) in the constructor"); + return -1; + } +} + + +/** + * Static global structure => the fields that are beyond the last + * initialized field are set to zero. + * https://stackoverflow.com/a/11152199/881731 + **/ +static PyTypeObject sdk_{{class_name}}_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "orthanc.{{short_name}}", /* tp_name */ + sizeof(sdk_{{class_name}}_Object), /* tp_basicsize */ +}; + + +{{#destructor}} +static void sdk_{{class_name}}_Destructor(PyObject *self) +{ + OrthancPlugins::LogInfo("Destroying Python object of class {{class_name}}"); + + sdk_{{class_name}}_Object& tmp = *((sdk_{{class_name}}_Object*) self); + + if (tmp.object_ != NULL && + !tmp.borrowed_) + { + {{destructor}}(OrthancPlugins::GetGlobalContext(), tmp.object_); + tmp.object_ = NULL; + } + + Py_TYPE(self)->tp_free((PyObject *)self); +} +{{/destructor}} + + +// Actual implementation of the methods +{{#methods}} +static PyObject *sdk_{{class_name}}_{{c_function}}( + sdk_{{class_name}}_Object* self, PyObject *args) +{ + OrthancPlugins::LogInfo("Calling method {{c_function}}() on object of class {{class_name}}"); + + if (self->object_ == NULL) + { + // TODO: RAISE + //PythonLock::RaiseException(module, OrthancPluginErrorCode_NullPointer); + PyErr_SetString(PyExc_ValueError, "Invalid object"); + return NULL; + } + +{{> function_body}} +} + +{{/methods}} + + +static void Register{{class_name}}Class(PyObject* module) +{ + sdk_{{class_name}}_Type.tp_new = PyType_GenericNew; + sdk_{{class_name}}_Type.tp_flags = Py_TPFLAGS_DEFAULT; + sdk_{{class_name}}_Type.tp_doc = "Generated from Orthanc C class: {{class_name}}"; + sdk_{{class_name}}_Type.tp_methods = sdk_{{class_name}}_Methods; + sdk_{{class_name}}_Type.tp_init = (initproc) sdk_{{class_name}}_Constructor; + +{{#destructor}} + /** + * "tp_dealloc is called when the reference count of the object goes + * down to zero. This is where you destroy the object and its + * members. It should then free the memory occupied by the object by + * calling tp_free." + * https://stackoverflow.com/a/24863227/881731 + **/ + sdk_{{class_name}}_Type.tp_dealloc = sdk_{{class_name}}_Destructor; +{{/destructor}} + + if (PyType_Ready(&sdk_{{class_name}}_Type) < 0) + { + OrthancPlugins::LogError("Cannot register Python class: {{class_name}}"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + Py_INCREF(&sdk_{{class_name}}_Type); + if (PyModule_AddObject(module, "{{short_name}}", (PyObject *)&sdk_{{class_name}}_Type) < 0) + { + OrthancPlugins::LogError("Cannot register Python class: {{class_name}}"); + Py_DECREF(&sdk_{{class_name}}_Type); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } +} + + +PyObject* Get{{class_name}}Type() +{ + return (PyObject*) &sdk_{{class_name}}_Type; +} diff -r 000000000000 -r 7ed502b17b8f CodeAnalysis/Enumeration.mustache --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CodeAnalysis/Enumeration.mustache Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,77 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +typedef struct +{ + PyObject_HEAD +} sdk_{{name}}_Object; + + +/** + * Static global structure => the fields that are beyond the last + * initialized field are set to zero. + * https://stackoverflow.com/a/11152199/881731 + **/ +static PyTypeObject sdk_{{name}}_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "orthanc.{{short_name}}", /* tp_name */ + sizeof(sdk_{{name}}_Object), /* tp_basicsize */ +}; + + +void Register{{name}}Enumeration(PyObject* module) +{ + sdk_{{name}}_Type.tp_new = PyType_GenericNew; + sdk_{{name}}_Type.tp_flags = Py_TPFLAGS_DEFAULT; + sdk_{{name}}_Type.tp_doc = "Generated from C enumeration OrthancPlugin{{name}}"; + + sdk_{{name}}_Type.tp_dict = PyDict_New(); + + if (PyType_Ready(&sdk_{{name}}_Type) < 0) + { + OrthancPlugins::LogError("Cannot register Python enumeration: {{name}}"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + /** + * Declare constants here (static members = class attributes) + * https://stackoverflow.com/a/8017906/881731 + * + * "Static and class methods can be defined in tp_methods by adding + * METH_STATIC or METH_CLASS to the ml_flags field of the + * PyMethodDef structure. This is equivalent to @staticmethod and + * @classmethod decorators." + * + * "Class attributes can be added by setting the tp_dict to a + * dictionary with these attributes before calling PyType_Ready() + * (in your module initialization function)." + **/ + + {{#values}} + PyDict_SetItemString(sdk_{{name}}_Type.tp_dict, "{{key}}", PyLong_FromLong({{value}})); + {{/values}} + + Py_INCREF(&sdk_{{name}}_Type); + if (PyModule_AddObject(module, "{{short_name}}", (PyObject *)&sdk_{{name}}_Type) < 0) + { + OrthancPlugins::LogError("Cannot register Python enumeration: {{name}}"); + Py_DECREF(&sdk_{{name}}_Type); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } +} diff -r 000000000000 -r 7ed502b17b8f CodeAnalysis/FunctionBody.mustache --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CodeAnalysis/FunctionBody.mustache Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,110 @@ +{{#args}} + {{python_type}} {{name}}{{initialization}}; +{{/args}} + +{{#has_args}} + if (!PyArg_ParseTuple(args, {{tuple_format}})) + { + // TODO => RAISE : https://stackoverflow.com/questions/60832317 + PyErr_SetString(PyExc_TypeError, "Bad types for the arguments ({{count_args}} arguments expected)"); + return NULL; + } +{{/has_args}} +{{#return_long}} + long value = {{c_function}}(OrthancPlugins::GetGlobalContext(){{self}}{{call_args}}); + {{#args}}{{release}}{{/args}} + return PyLong_FromLong(value); +{{/return_long}} +{{#return_static_string}} + const char* s = {{c_function}}(OrthancPlugins::GetGlobalContext(){{self}}{{call_args}}); + {{#args}}{{release}}{{/args}} + if (s == NULL) + { + Py_INCREF(Py_None); + return Py_None; + } + else + { + return PyUnicode_FromString(s); + } +{{/return_static_string}} +{{#return_dynamic_string}} + OrthancPlugins::OrthancString s; + s.Assign({{c_function}}(OrthancPlugins::GetGlobalContext(){{self}}{{call_args}})); + {{#args}}{{release}}{{/args}} + if (s.GetContent() == NULL) + { + // TODO => RAISE : https://stackoverflow.com/questions/60832317 + //PythonLock::RaiseException(module, OrthancPluginErrorCode_InternalError); + PyErr_SetString(PyExc_ValueError, "Internal error"); + return NULL; + } + else + { + return PyUnicode_FromString(s.GetContent()); + } +{{/return_dynamic_string}} +{{#return_void}} + {{c_function}}(OrthancPlugins::GetGlobalContext(){{self}}{{call_args}}); + {{#args}}{{release}}{{/args}} + + Py_INCREF(Py_None); + return Py_None; +{{/return_void}} +{{#return_error}} + OrthancPluginErrorCode code = {{c_function}}(OrthancPlugins::GetGlobalContext(){{self}}{{call_args}}); + {{#args}}{{release}}{{/args}} + + if (code == OrthancPluginErrorCode_Success) + { + Py_INCREF(Py_None); + return Py_None; + } + else + { + // TODO => RAISE : https://stackoverflow.com/questions/60832317 + //PythonLock::RaiseException(module, code); + PyErr_SetString(PyExc_ValueError, "Internal error"); + return NULL; + } +{{/return_error}} +{{#return_object}} + // This is the case of a constructor + {{return_object}}* obj = {{c_function}}(OrthancPlugins::GetGlobalContext(){{self}}{{call_args}}); + {{#args}}{{release}}{{/args}} + if (obj == NULL) + { + // TODO => RAISE : https://stackoverflow.com/questions/60832317 + //PythonLock::RaiseException(module, OrthancPluginErrorCode_InternalError); + PyErr_SetString(PyExc_ValueError, "Internal error"); + return NULL; + } + else + { + PyObject *argList = Py_BuildValue("Lb", obj, false /* not borrowed */); + PyObject *python = PyObject_CallObject((PyObject *) &sdk_{{return_object}}_Type, argList); + Py_DECREF(argList); + return python; + } +{{/return_object}} +{{#return_bytes}} + OrthancPlugins::MemoryBuffer buffer; + OrthancPluginErrorCode code = {{c_function}}(OrthancPlugins::GetGlobalContext(), *buffer{{self}}{{call_args}}); + {{#args}}{{release}}{{/args}} + if (code == OrthancPluginErrorCode_Success) + { + return PyBytes_FromStringAndSize(buffer.GetData(), buffer.GetSize()); + } + else + { + // TODO => RAISE : https://stackoverflow.com/questions/60832317 + //PythonLock::RaiseException(module, OrthancPluginErrorCode_InternalError); + PyErr_SetString(PyExc_ValueError, "Internal error"); + return NULL; + } +{{/return_bytes}} +{{#return_enumeration}} + {{return_enumeration}} value = {{c_function}}(OrthancPlugins::GetGlobalContext(){{self}}{{call_args}}); + {{#args}}{{release}}{{/args}} + return PyLong_FromLong(value); +{{/return_enumeration}} diff -r 000000000000 -r 7ed502b17b8f CodeAnalysis/GlobalFunctions.mustache --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CodeAnalysis/GlobalFunctions.mustache Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,43 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +{{#global_functions}} +static PyObject* sdk_{{c_function}}(PyObject* module, PyObject* args) +{ + OrthancPlugins::LogInfo("Calling Python global function: {{c_function}}()"); + +{{> function_body}} +} + +{{/global_functions}} + +static PyMethodDef ORTHANC_SDK_FUNCTIONS[] = +{ +{{#global_functions}} + { "{{short_name}}", sdk_{{c_function}}, METH_VARARGS, + "Generated from C function {{c_function}}()" }, +{{/global_functions}} + { NULL, NULL } +}; + + +PyMethodDef* GetOrthancSdkFunctions() +{ + return ORTHANC_SDK_FUNCTIONS; +} diff -r 000000000000 -r 7ed502b17b8f CodeAnalysis/ParseOrthancSDK.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CodeAnalysis/ParseOrthancSDK.py Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,468 @@ +#!/usr/bin/env python + +## +## Python plugin for Orthanc +## Copyright (C) 2017-2020 Osimis S.A., Belgium +## +## 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 . +## + + +# Ubuntu 18.04: +# sudo apt-get install python-clang-4.0 +# python2 ./ParseOrthancSDK.py --libclang=libclang-4.0.so.1 ../Resources/Orthanc/Sdk-1.5.7/orthanc/OrthancCPlugin.h ../Sources/Autogenerated + + +import argparse +import clang.cindex +import os +import pprint +import pystache +import sys + + +ROOT = os.path.dirname(os.path.realpath(sys.argv[0])) + + +## +## Parse the command-line arguments +## + +parser = argparse.ArgumentParser(description = 'Parse the Orthanc SDK.') +parser.add_argument('--libclang', + default = 'libclang-4.0.so.1', + help = 'manually provides the path to the libclang shared library') +parser.add_argument('--source', + default = os.path.join(os.path.dirname(__file__), + '../Resources/Orthanc/Sdk-1.5.7/orthanc/OrthancCPlugin.h'), + help = 'Input C++ file') +parser.add_argument('--target', + default = os.path.join(os.path.dirname(__file__), + '../Sources/Autogenerated'), + help = 'Target folder') + +args = parser.parse_args() + + + +if len(args.libclang) != 0: + clang.cindex.Config.set_library_file(args.libclang) + +index = clang.cindex.Index.create() + +tu = index.parse(args.source, [ ]) + +TARGET = os.path.realpath(args.target) + + + +def ToUpperCase(name): + s = '' + for i in range(len(name)): + if name[i].isupper(): + if len(s) == 0: + s += name[i] + elif name[i - 1].islower(): + s += '_' + name[i] + elif (i + 1 < len(name) and + name[i - 1].islower() and + name[i + 1].isupper()): + s += '_' + name[i] + else: + s += name[i] + else: + s += name[i].upper() + return s + + + +with open(os.path.join(ROOT, 'Enumeration.mustache'), 'r') as f: + TEMPLATE = f.read() + + +classes = {} +enumerations = {} +globalFunctions = [] + +def IsSourceStringType(t): + return (t.kind == clang.cindex.TypeKind.POINTER and + t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S and + t.get_pointee().is_const_qualified()) + +def IsTargetStaticStringType(t): + return (t.kind == clang.cindex.TypeKind.POINTER and + t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S and + t.get_pointee().is_const_qualified()) + +def IsTargetDynamicStringType(t): + return (t.kind == clang.cindex.TypeKind.POINTER and + t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S and + not t.get_pointee().is_const_qualified()) + +def IsIntegerType(t): + return (t.kind == clang.cindex.TypeKind.INT or + t.spelling in [ 'int8_t', 'int16_t', 'int32_t', 'int64_t', + 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t']) + +def IsFloatType(t): + return t.kind == clang.cindex.TypeKind.FLOAT + +def IsEnumerationType(t): + return (t.kind == clang.cindex.TypeKind.TYPEDEF and + t.spelling in enumerations) + +def IsTargetMemoryBufferType(t): + return (t.kind == clang.cindex.TypeKind.POINTER and + not t.get_pointee().is_const_qualified() and + t.get_pointee().spelling == 'OrthancPluginMemoryBuffer') + +def IsSourceMemoryBufferType(t): + return (t.kind == clang.cindex.TypeKind.POINTER and + t.get_pointee().kind == clang.cindex.TypeKind.VOID and + t.get_pointee().is_const_qualified()) + +def IsClassType(t): + return (t.kind == clang.cindex.TypeKind.POINTER and + ((t.get_pointee().is_const_qualified() and + t.get_pointee().spelling.startswith('const ') and + t.get_pointee().spelling[len('const '):] in classes) or + (not t.get_pointee().is_const_qualified() and + t.get_pointee().spelling in classes))) + +def IsSimpleSourceType(t): + return (IsSourceStringType(t) or + IsFloatType(t) or + IsIntegerType(t) or + IsEnumerationType(t) or + IsSourceMemoryBufferType(t)) + +def IsVoidType(t): + return t.kind == clang.cindex.TypeKind.VOID + +def IsSupportedTargetType(t): + return (IsVoidType(t) or + IsIntegerType(t) or + IsEnumerationType(t) or + # Constructor of a class + (t.kind == clang.cindex.TypeKind.POINTER and + not t.get_pointee().is_const_qualified() and + t.get_pointee().spelling in classes) or + # "const char*" or "char*" outputs + (t.kind == clang.cindex.TypeKind.POINTER and + #not t.get_pointee().is_const_qualified() and + t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S)) + +def IsBytesArgument(args, index): + return (index + 1 < len(args) and + args[index].type.kind == clang.cindex.TypeKind.POINTER and + args[index].type.get_pointee().kind == clang.cindex.TypeKind.VOID and + args[index].type.get_pointee().is_const_qualified() and + args[index + 1].type.spelling == 'uint32_t') + +def CheckOnlySupportedArguments(args): + j = 0 + while j < len(args): + if IsBytesArgument(args, j): + j += 2 + elif IsSimpleSourceType(args[j].type): + j += 1 + else: + return False + return True + + +ORTHANC_TO_PYTHON_NUMERIC_TYPES = { + # https://docs.python.org/3/c-api/arg.html#numbers + 'int' : { + 'type' : 'int', + 'format' : 'i', + }, + 'uint8_t' : { + 'type' : 'unsigned char', + 'format' : 'b', + }, + 'int32_t' : { + 'type' : 'long int', + 'format' : 'l', + }, + 'uint16_t' : { + 'type' : 'unsigned short', + 'format' : 'H', + }, + 'uint32_t' : { + 'type' : 'unsigned long', + 'format' : 'k', + }, + 'uint64_t' : { + 'type' : 'unsigned long long', + 'format' : 'K', + }, + 'float' : { + 'type' : 'float', + 'format' : 'f', + } + } + + +def GenerateFunctionBodyTemplate(cFunction, result_type, args): + if not cFunction.startswith('OrthancPlugin'): + raise Exception() + + func = { + 'c_function' : cFunction, + 'short_name' : cFunction[len('OrthancPlugin'):], + 'args' : [], + } + + if IsIntegerType(result_type): + func['return_long'] = True + elif IsTargetDynamicStringType(result_type): + func['return_dynamic_string'] = True + elif IsTargetStaticStringType(result_type): + func['return_static_string'] = True + elif IsVoidType(result_type): + func['return_void'] = True + elif result_type.spelling == 'OrthancPluginErrorCode': + func['return_error'] = True + elif IsClassType(result_type): + func['return_object'] = result_type.get_pointee().spelling + elif IsTargetMemoryBufferType(result_type): + func['return_bytes'] = True + elif IsEnumerationType(result_type): + func['return_enumeration'] = result_type.spelling + else: + raise Exception('Not supported: %s' % result_type.spelling) + + i = 0 + while i < len(args): + a = { + 'name' : 'arg%d' % i, + } + + if (IsIntegerType(args[i].type) or + IsFloatType(args[i].type)): + t = ORTHANC_TO_PYTHON_NUMERIC_TYPES[args[i].type.spelling] + a['python_type'] = t['type'] + a['python_format'] = t['format'] + a['initialization'] = ' = 0' + a['orthanc_cast'] = 'arg%d' % i + func['args'].append(a) + elif IsSourceStringType(args[i].type): + a['python_type'] = 'const char*' + a['python_format'] = 's' + a['initialization'] = ' = NULL' + a['orthanc_cast'] = 'arg%d' % i + func['args'].append(a) + elif IsEnumerationType(args[i].type): + a['python_type'] = 'long int' + a['python_format'] = 'l' + a['initialization'] = ' = 0' + a['orthanc_cast'] = 'static_cast<%s>(arg%d)' % (args[i].type.spelling, i) + func['args'].append(a) + elif IsBytesArgument(args, i): + a['python_type'] = 'Py_buffer' + # In theory, one should use "y*" (this is the recommended + # way to accept binary data). However, this is not + # available in Python 2.7 + a['python_format'] = 's*' + a['orthanc_cast'] = 'arg%d.buf, arg%d.len' % (i, i) + a['release'] = 'PyBuffer_Release(&arg%d);' % i + func['args'].append(a) + i += 1 + elif IsSourceMemoryBufferType(args[i].type): + a['python_type'] = 'Py_buffer' + a['python_format'] = 's*' + a['orthanc_cast'] = 'arg%d.buf' % i + a['release'] = 'PyBuffer_Release(&arg%d);' % i + func['args'].append(a) + else: + raise Exception('Not supported: %s, %s' % (cFunction, args[i].spelling)) + + i += 1 + + func['tuple_format'] = '"%s", %s' % ( + ''.join(map(lambda x: x['python_format'], func['args'])), + ', '.join(map(lambda x: '&' + x['name'], func['args']))) + + if len(func['args']) > 0: + func['count_args'] = len(func['args']) + func['has_args'] = True + func['call_args'] = ', ' + ', '.join(map(lambda x: x['orthanc_cast'], func['args'])) + + return func + + +for node in tu.cursor.get_children(): + if node.kind == clang.cindex.CursorKind.ENUM_DECL: + if node.type.spelling.startswith('OrthancPlugin'): + name = node.type.spelling + + values = [] + for item in node.get_children(): + if (item.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL and + item.spelling.startswith(name + '_')): + values.append({ + 'key' : ToUpperCase(item.spelling[len(name)+1:]), + 'value' : item.enum_value + }) + + path = 'sdk_%s.impl.h' % name + shortName = name[len('OrthancPlugin'):] + + with open(os.path.join(TARGET, path), 'w') as f: + f.write(pystache.render(TEMPLATE, { + 'name' : name, + 'short_name' : shortName, + 'values' : values, + })) + + enumerations[name] = { + 'name' : name, + 'path' : path, + } + + elif node.kind == clang.cindex.CursorKind.FUNCTION_DECL: + if node.spelling.startswith('OrthancPlugin'): + #if node.spelling != 'OrthancPluginWorklistGetDicomQuery': + # continue + shortName = node.spelling[len('OrthancPlugin'):] + + # Check that the first argument is the Orthanc context + args = list(filter(lambda x: x.kind == clang.cindex.CursorKind.PARM_DECL, + node.get_children())) + + if (len(args) == 0 or + args[0].type.kind != clang.cindex.TypeKind.POINTER or + args[0].type.get_pointee().spelling != 'OrthancPluginContext'): + print('Not in the Orthanc SDK: %s()' % node.spelling) + continue + + # Discard the context from the arguments + args = args[1:] + + if not IsSupportedTargetType(node.result_type): + print('*** UNSUPPORTED OUTPUT: %s' % node.spelling) + + elif (len(args) == 1 and + IsClassType(args[0].type) and + node.spelling.startswith('OrthancPluginFree')): + print('Destructor: %s' % node.spelling) + className = args[0].type.get_pointee().spelling + classes[className]['destructor'] = node.spelling + + elif CheckOnlySupportedArguments(args): + if IsClassType(node.result_type): + print('Constructor: %s' % node.spelling) + else: + print('Simple global function: %s => %s' % (node.spelling, node.result_type.spelling)) + + body = GenerateFunctionBodyTemplate(node.spelling, node.result_type, args) + globalFunctions.append(body) + + elif (len(args) >= 2 and + IsTargetMemoryBufferType(args[0].type) and + CheckOnlySupportedArguments(args[1:])): + print('Simple global function, returning bytes: %s' % node.spelling) + + body = GenerateFunctionBodyTemplate(node.spelling, args[0].type, args[1:]) + globalFunctions.append(body) + + elif (IsClassType(args[0].type) and + CheckOnlySupportedArguments(args[1:])): + className = args[0].type.get_pointee().spelling + + if className.startswith('const '): + className = className[len('const '):] + + print('Simple method of class %s: %s' % (className, node.spelling)) + + method = GenerateFunctionBodyTemplate(node.spelling, node.result_type, args[1:]) + method['self'] = ', self->object_' + classes[className]['methods'].append(method) + + elif (len(args) >= 2 and + IsTargetMemoryBufferType(args[0].type) and + IsClassType(args[1].type) and + CheckOnlySupportedArguments(args[2:])): + print('Simple method of class %s, returning bytes: %s' % ( + args[0].type.get_pointee().spelling, + node.spelling)) + + method = GenerateFunctionBodyTemplate(node.spelling, args[0].type, args[2:]) + method['self'] = ', self->object_' + classes[className]['methods'].append(method) + + else: + print('*** UNSUPPORTED INPUT: %s' % node.spelling) + + + + elif node.kind == clang.cindex.CursorKind.STRUCT_DECL: + if (node.spelling.startswith('_OrthancPlugin') and + node.spelling.endswith('_t') and + node.spelling != '_OrthancPluginContext_t'): + name = node.spelling[len('_') : -len('_t')] + classes[name] = { + 'class_name' : name, + 'short_name' : name[len('OrthancPlugin'):], + 'methods' : [ ] + } + + + + +partials = {} + +with open(os.path.join(ROOT, 'FunctionBody.mustache'), 'r') as f: + partials['function_body'] = f.read() + +renderer = pystache.Renderer( + escape = lambda u: u, # No escaping + partials = partials, +) + +with open(os.path.join(ROOT, 'Class.mustache'), 'r') as f: + template = f.read() + + for (key, value) in classes.items(): + with open(os.path.join(TARGET, 'sdk_%s.impl.h' % value['class_name']), 'w') as h: + h.write(renderer.render(template, value)) + + +def FlattenDictionary(source): + result = [] + for (key, value) in source.items(): + result.append(value) + return result + + +with open(os.path.join(ROOT, 'GlobalFunctions.mustache'), 'r') as f: + with open(os.path.join(TARGET, 'sdk_GlobalFunctions.impl.h'), 'w') as h: + h.write(renderer.render(f.read(), { + 'global_functions' : globalFunctions, + })) + +with open(os.path.join(ROOT, 'sdk.cpp.mustache'), 'r') as f: + with open(os.path.join(TARGET, 'sdk.cpp'), 'w') as h: + h.write(renderer.render(f.read(), { + 'classes' : FlattenDictionary(classes), + 'enumerations' : FlattenDictionary(enumerations), + 'global_functions' : globalFunctions, + })) + +with open(os.path.join(ROOT, 'sdk.h.mustache'), 'r') as f: + with open(os.path.join(TARGET, 'sdk.h'), 'w') as h: + h.write(renderer.render(f.read(), { + 'classes' : FlattenDictionary(classes), + })) diff -r 000000000000 -r 7ed502b17b8f CodeAnalysis/sdk.cpp.mustache --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CodeAnalysis/sdk.cpp.mustache Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,43 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#include "sdk.h" + +#include + +{{#enumerations}} +#include "./sdk_{{name}}.impl.h" +{{/enumerations}} + +{{#classes}} +#include "./sdk_{{class_name}}.impl.h" +{{/classes}} + +#include "./sdk_GlobalFunctions.impl.h" + +void RegisterOrthancSdk(PyObject* module) +{ +{{#enumerations}} + Register{{name}}Enumeration(module); +{{/enumerations}} + +{{#classes}} + Register{{class_name}}Class(module); +{{/classes}} +} diff -r 000000000000 -r 7ed502b17b8f CodeAnalysis/sdk.h.mustache --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CodeAnalysis/sdk.h.mustache Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,29 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#pragma once + +#include + +void RegisterOrthancSdk(PyObject* module); +PyMethodDef* GetOrthancSdkFunctions(); + +{{#classes}} +PyObject* Get{{class_name}}Type(); +{{/classes}} diff -r 000000000000 -r 7ed502b17b8f README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,46 @@ +Python plugin for Orthanc +========================= + + +General Information +------------------- + +This repository contains the source code of a plugin to create new +plugins for Orthanc using the Python programming language. + + +Installation and usage +---------------------- + +Build instructions are similar to that of Orthanc: +http://book.orthanc-server.com/faq/compiling.html + +Usage details are available as part of the Orthanc Book: +http://book.orthanc-server.com/plugins/python.html + + +Licensing +--------- + +The Python plugin for Orthanc is licensed under the AGPL license. + +We also kindly ask scientific works and clinical studies that make +use of Orthanc to cite Orthanc in their associated publications. +Similarly, we ask open-source and closed-source products that make +use of Orthanc to warn us about this use. You can cite our work +using the following BibTeX entry: + +@Article{Jodogne2018, + author="Jodogne, S{\'e}bastien", + title="The {O}rthanc Ecosystem for Medical Imaging", + journal="Journal of Digital Imaging", + year="2018", + month="Jun", + day="01", + volume="31", + number="3", + pages="341--352", + issn="1618-727X", + doi="10.1007/s10278-018-0082-y", + url="https://doi.org/10.1007/s10278-018-0082-y" +} diff -r 000000000000 -r 7ed502b17b8f Resources/Builders/Docker/Dockerfile-BuildEnvironment --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Builders/Docker/Dockerfile-BuildEnvironment Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,9 @@ +FROM debian:stable-slim + +MAINTAINER Sebastien Jodogne +LABEL Description="Orthanc, free DICOM server" Vendor="The Orthanc project" + +RUN apt-get -y clean && apt-get -y update +RUN DEBIAN_FRONTEND=noninteractive apt-get -y install \ + nano build-essential unzip cmake pkg-config libpython3.7-dev && \ + apt-get clean && rm -rf /var/lib/apt/lists/* diff -r 000000000000 -r 7ed502b17b8f Resources/Builders/Docker/Dockerfile-Release --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Builders/Docker/Dockerfile-Release Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,11 @@ +FROM jodogne/orthanc-plugins:1.6.0 + +MAINTAINER Sebastien Jodogne +LABEL Description="Orthanc, free DICOM server" Vendor="The Orthanc project" + +RUN apt-get -y clean && apt-get -y update +RUN DEBIAN_FRONTEND=noninteractive apt-get -y install python3.7 libpython3.7 && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +ADD ./docker-build/share/orthanc/plugins/libOrthancPython.so \ + /usr/local/share/orthanc/plugins/libOrthancPython.so diff -r 000000000000 -r 7ed502b17b8f Resources/Builders/Docker/NOTES.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Builders/Docker/NOTES.txt Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,12 @@ + + +Create the Docker image: + +$ ./docker-compile.sh Release +$ docker build -f Dockerfile-Release -t python-plugin ../../.. + + +Run the just-created Docker image: + +$ docker run -p 4242:4242 -p 8042:8042 --rm python-plugin + diff -r 000000000000 -r 7ed502b17b8f Resources/Builders/Docker/docker-compile.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Builders/Docker/docker-compile.sh Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,30 @@ +#!/bin/bash + +set -ex + +if [ "$1" != "Debug" -a "$1" != "Release" ]; then + echo "Please provide build type: Debug or Release" + exit -1 +fi + +if [ -t 1 ]; then + # TTY is available => use interactive mode + DOCKER_FLAGS='-i' +fi + +ROOT_DIR=`dirname $(readlink -f $0)`/../../.. + +mkdir -p ${ROOT_DIR}/docker-build/ + +docker build \ + -f ${ROOT_DIR}/Resources/Builders/Docker/Dockerfile-BuildEnvironment \ + -t debian-stable-python-build . + +docker run -t ${DOCKER_FLAGS} --rm \ + --user $(id -u):$(id -g) \ + -v ${ROOT_DIR}:/source:ro \ + -v ${ROOT_DIR}/docker-build:/target:rw \ + debian-stable-python-build \ + bash /source/Resources/Builders/Docker/docker-internal.sh $1 + +ls -lR ${ROOT_DIR}/docker-build/ diff -r 000000000000 -r 7ed502b17b8f Resources/Builders/Docker/docker-internal.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Builders/Docker/docker-internal.sh Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,19 @@ +#!/bin/bash +set -ex + +mkdir /tmp/build +cd /tmp/build + +cmake /source \ + -DCMAKE_BUILD_TYPE=$1 \ + -DPYTHON_VERSION=3.7 \ + -DSTATIC_BUILD=ON \ + -DCMAKE_INSTALL_PREFIX=/target + +make -j`nproc` + +if [ "$1" == "Release" ]; then + strip ./libOrthancPython.so +fi + +make install diff -r 000000000000 -r 7ed502b17b8f Resources/Builders/Dockerfile-MinGW-BuildEnvironment --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Builders/Dockerfile-MinGW-BuildEnvironment Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,9 @@ +FROM debian:stable-slim + +MAINTAINER Sebastien Jodogne +LABEL Description="Orthanc, free DICOM server" Vendor="The Orthanc project" + +RUN apt-get -y clean && apt-get -y update +RUN DEBIAN_FRONTEND=noninteractive apt-get -y install \ + nano unzip cmake mingw-w64 patch wget bzip2 && \ + apt-get clean && rm -rf /var/lib/apt/lists/* diff -r 000000000000 -r 7ed502b17b8f Resources/Builders/Windows32-Python2.7/docker-compile.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Builders/Windows32-Python2.7/docker-compile.sh Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,30 @@ +#!/bin/bash + +set -ex + +if [ "$1" != "Debug" -a "$1" != "Release" ]; then + echo "Please provide build type: Debug or Release" + exit -1 +fi + +if [ -t 1 ]; then + # TTY is available => use interactive mode + DOCKER_FLAGS='-i' +fi + +ROOT_DIR=`dirname $(readlink -f $0)`/../../.. + +mkdir -p ${ROOT_DIR}/docker-build/ + +docker build \ + -f ${ROOT_DIR}/Resources/Builders/Dockerfile-MinGW-BuildEnvironment \ + -t mingw-python-build . + +docker run -t ${DOCKER_FLAGS} --rm \ + --user $(id -u):$(id -g) \ + -v ${ROOT_DIR}:/source:ro \ + -v ${ROOT_DIR}/docker-build:/target:rw \ + mingw-python-build \ + bash /source/Resources/Builders/Windows32-Python2.7/docker-internal.sh $1 + +ls -lR ${ROOT_DIR}/docker-build/ diff -r 000000000000 -r 7ed502b17b8f Resources/Builders/Windows32-Python2.7/docker-internal.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Builders/Windows32-Python2.7/docker-internal.sh Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,24 @@ +#!/bin/bash +set -ex + +mkdir /tmp/build +cd /tmp/build + +wget http://orthanc.osimis.io/ThirdPartyDownloads/Python/python-2.7.17-win32.tar.bz2 +tar xvfj python-2.7.17-win32.tar.bz2 + +cmake /source \ + -DCMAKE_BUILD_TYPE=$1 \ + -DSTATIC_BUILD=ON \ + -DPYTHON_VERSION=2.7 \ + -DPYTHON_WINDOWS_ROOT=/tmp/build/python-2.7.17-win32/ \ + -DCMAKE_TOOLCHAIN_FILE=/source/Resources/Orthanc/Resources/MinGW-W64-Toolchain32.cmake \ + -DCMAKE_INSTALL_PREFIX=/target + +make -j`nproc` VERBOSE=1 + +if [ "$1" == "Release" ]; then + i686-w64-mingw32-strip ./libOrthancPython.dll +fi + +make install diff -r 000000000000 -r 7ed502b17b8f Resources/OrthancCPlugin-1.5.7.patch --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/OrthancCPlugin-1.5.7.patch Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,59 @@ +--- OrthancCPlugin.h.orig 2020-03-26 16:23:11.963320239 +0100 ++++ OrthancCPlugin.h 2020-03-26 16:24:45.403002064 +0100 +@@ -909,14 +909,14 @@ + **/ + typedef enum + { +- OrthancPluginMetricsType_Default, /*!< Default metrics */ ++ OrthancPluginMetricsType_Default = 0, /*!< Default metrics */ + + /** + * This metrics represents a time duration. Orthanc will keep the + * maximum value of the metrics over a sliding window of ten + * seconds, which is useful if the metrics is sampled frequently. + **/ +- OrthancPluginMetricsType_Timer ++ OrthancPluginMetricsType_Timer = 1 + } OrthancPluginMetricsType; + + +@@ -926,9 +926,9 @@ + **/ + typedef enum + { +- OrthancPluginDicomWebBinaryMode_Ignore, /*!< Don't include binary tags */ +- OrthancPluginDicomWebBinaryMode_InlineBinary, /*!< Inline encoding using Base64 */ +- OrthancPluginDicomWebBinaryMode_BulkDataUri /*!< Use a bulk data URI field */ ++ OrthancPluginDicomWebBinaryMode_Ignore = 0, /*!< Don't include binary tags */ ++ OrthancPluginDicomWebBinaryMode_InlineBinary = 1, /*!< Inline encoding using Base64 */ ++ OrthancPluginDicomWebBinaryMode_BulkDataUri = 2 /*!< Use a bulk data URI field */ + } OrthancPluginDicomWebBinaryMode; + + +@@ -1915,7 +1915,7 @@ + typedef struct + { + OrthancPluginRestOutput* output; +- const char* answer; ++ const void* answer; + uint32_t answerSize; + const char* mimeType; + } _OrthancPluginAnswerBuffer; +@@ -1935,7 +1935,7 @@ + ORTHANC_PLUGIN_INLINE void OrthancPluginAnswerBuffer( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, +- const char* answer, ++ const void* answer, + uint32_t answerSize, + const char* mimeType) + { +@@ -2732,7 +2732,7 @@ + * @return The pointer to the DICOM data, NULL in case of error. + * @ingroup Callbacks + **/ +- ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceData( ++ ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData( + OrthancPluginContext* context, + OrthancPluginDicomInstance* instance) + { diff -r 000000000000 -r 7ed502b17b8f Resources/SyncOrthancFolder.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/SyncOrthancFolder.py Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,84 @@ +#!/usr/bin/python + +# +# This maintenance script updates the content of the "Orthanc" folder +# to match the latest version of the Orthanc source code. +# + +import multiprocessing +import os +import stat +import urllib2 +import subprocess + +TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc') +PLUGIN_SDK_VERSION = '1.5.7' +REPOSITORY = 'https://hg.orthanc-server.com/orthanc/raw-file' + +FILES = [ + 'Core/Compatibility.h', + 'Plugins/Samples/Common/OrthancPluginCppWrapper.cpp', + 'Plugins/Samples/Common/OrthancPluginCppWrapper.h', + 'Plugins/Samples/Common/OrthancPluginException.h', + 'Plugins/Samples/Common/VersionScript.map', + 'Resources/CMake/BoostConfiguration.cmake', + 'Resources/CMake/Compiler.cmake', + 'Resources/CMake/DownloadPackage.cmake', + 'Resources/CMake/JsonCppConfiguration.cmake', + 'Resources/LinuxStandardBaseToolchain.cmake', + 'Resources/MinGW-W64-Toolchain32.cmake', + 'Resources/MinGW-W64-Toolchain64.cmake', + 'Resources/MinGWToolchain.cmake', + 'Resources/Patches/boost-1.69.0-linux-standard-base.patch', +] + +SDK = [ + 'orthanc/OrthancCPlugin.h', +] + + +def Download(x): + branch = x[0] + source = x[1] + target = os.path.join(TARGET, x[2]) + print target + + try: + os.makedirs(os.path.dirname(target)) + except: + pass + + url = '%s/%s/%s' % (REPOSITORY, branch, source) + print('Downloading %s' % url) + + with open(target, 'w') as f: + f.write(urllib2.urlopen(url).read()) + + print('Done %s' % url) + + + +commands = [] + +for f in FILES: + commands.append([ 'default', f, f ]) + +for f in SDK: + commands.append([ + 'Orthanc-%s' % PLUGIN_SDK_VERSION, + 'Plugins/Include/%s' % f, + 'Sdk-%s/%s' % (PLUGIN_SDK_VERSION, f) + ]) + + +pool = multiprocessing.Pool(10) # simultaneous downloads +pool.map(Download, commands) + + +# Patch the SDK +subprocess.check_call([ 'patch', '-p0', '-i', os.path.join + (os.path.abspath(os.path.dirname(__file__)), + 'OrthancCPlugin-%s.patch' % PLUGIN_SDK_VERSION) ], + cwd = os.path.join(os.path.dirname(__file__), + 'Orthanc', + 'Sdk-%s' % PLUGIN_SDK_VERSION, 'orthanc')) diff -r 000000000000 -r 7ed502b17b8f Resources/valgrind-python.supp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/valgrind-python.supp Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,391 @@ +# +# This is a valgrind suppression file that should be used when using valgrind. +# +# Here's an example of running valgrind: +# +# cd python/dist/src +# valgrind --tool=memcheck --suppressions=Misc/valgrind-python.supp \ +# ./python -E -tt ./Lib/test/regrtest.py -u bsddb,network +# +# You must edit Objects/obmalloc.c and uncomment Py_USING_MEMORY_DEBUGGER +# to use the preferred suppressions with Py_ADDRESS_IN_RANGE. +# +# If you do not want to recompile Python, you can uncomment +# suppressions for PyObject_Free and PyObject_Realloc. [ => DONE BY SJO ] +# +# See Misc/README.valgrind for more information. + +# all tool names: Addrcheck,Memcheck,cachegrind,helgrind,massif +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Addr4 + fun:Py_ADDRESS_IN_RANGE +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Value4 + fun:Py_ADDRESS_IN_RANGE +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 8 (x86_64 aka amd64) + Memcheck:Value8 + fun:Py_ADDRESS_IN_RANGE +} + +{ + ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value + Memcheck:Cond + fun:Py_ADDRESS_IN_RANGE +} + +# +# Leaks (including possible leaks) +# Hmmm, I wonder if this masks some real leaks. I think it does. +# Will need to fix that. +# + +{ + Suppress leaking the GIL. Happens once per process, see comment in ceval.c. + Memcheck:Leak + fun:malloc + fun:PyThread_allocate_lock + fun:PyEval_InitThreads +} + +{ + Suppress leaking the GIL after a fork. + Memcheck:Leak + fun:malloc + fun:PyThread_allocate_lock + fun:PyEval_ReInitThreads +} + +{ + Suppress leaking the autoTLSkey. This looks like it shouldn't leak though. + Memcheck:Leak + fun:malloc + fun:PyThread_create_key + fun:_PyGILState_Init + fun:Py_InitializeEx + fun:Py_Main +} + +{ + Hmmm, is this a real leak or like the GIL? + Memcheck:Leak + fun:malloc + fun:PyThread_ReInitTLS +} + +{ + Handle PyMalloc confusing valgrind (possibly leaked) + Memcheck:Leak + fun:realloc + fun:_PyObject_GC_Resize + fun:COMMENT_THIS_LINE_TO_DISABLE_LEAK_WARNING +} + +{ + Handle PyMalloc confusing valgrind (possibly leaked) + Memcheck:Leak + fun:malloc + fun:_PyObject_GC_New + fun:COMMENT_THIS_LINE_TO_DISABLE_LEAK_WARNING +} + +{ + Handle PyMalloc confusing valgrind (possibly leaked) + Memcheck:Leak + fun:malloc + fun:_PyObject_GC_NewVar + fun:COMMENT_THIS_LINE_TO_DISABLE_LEAK_WARNING +} + +# +# Non-python specific leaks +# + +{ + Handle pthread issue (possibly leaked) + Memcheck:Leak + fun:calloc + fun:allocate_dtv + fun:_dl_allocate_tls_storage + fun:_dl_allocate_tls +} + +{ + Handle pthread issue (possibly leaked) + Memcheck:Leak + fun:memalign + fun:_dl_allocate_tls_storage + fun:_dl_allocate_tls +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Addr4 + fun:PyObject_Free +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Value4 + fun:PyObject_Free +} + +{ + ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value + Memcheck:Cond + fun:PyObject_Free +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Addr4 + fun:PyObject_Realloc +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Value4 + fun:PyObject_Realloc +} + +{ + ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value + Memcheck:Cond + fun:PyObject_Realloc +} + +### +### All the suppressions below are for errors that occur within libraries +### that Python uses. The problems to not appear to be related to Python's +### use of the libraries. +### + +{ + Generic ubuntu ld problems + Memcheck:Addr8 + obj:/lib/ld-2.4.so + obj:/lib/ld-2.4.so + obj:/lib/ld-2.4.so + obj:/lib/ld-2.4.so +} + +{ + Generic gentoo ld problems + Memcheck:Cond + obj:/lib/ld-2.3.4.so + obj:/lib/ld-2.3.4.so + obj:/lib/ld-2.3.4.so + obj:/lib/ld-2.3.4.so +} + +{ + DBM problems, see test_dbm + Memcheck:Param + write(buf) + fun:write + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + fun:dbm_close +} + +{ + DBM problems, see test_dbm + Memcheck:Value8 + fun:memmove + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + fun:dbm_store + fun:dbm_ass_sub +} + +{ + DBM problems, see test_dbm + Memcheck:Cond + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + fun:dbm_store + fun:dbm_ass_sub +} + +{ + DBM problems, see test_dbm + Memcheck:Cond + fun:memmove + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + fun:dbm_store + fun:dbm_ass_sub +} + +{ + GDBM problems, see test_gdbm + Memcheck:Param + write(buf) + fun:write + fun:gdbm_open + +} + +{ + ZLIB problems, see test_gzip + Memcheck:Cond + obj:/lib/libz.so.1.2.3 + obj:/lib/libz.so.1.2.3 + fun:deflate +} + +{ + Avoid problems w/readline doing a putenv and leaking on exit + Memcheck:Leak + fun:malloc + fun:xmalloc + fun:sh_set_lines_and_columns + fun:_rl_get_screen_size + fun:_rl_init_terminal_io + obj:/lib/libreadline.so.4.3 + fun:rl_initialize +} + +### +### These occur from somewhere within the SSL, when running +### test_socket_sll. They are too general to leave on by default. +### +###{ +### somewhere in SSL stuff +### Memcheck:Cond +### fun:memset +###} +###{ +### somewhere in SSL stuff +### Memcheck:Value4 +### fun:memset +###} +### +###{ +### somewhere in SSL stuff +### Memcheck:Cond +### fun:MD5_Update +###} +### +###{ +### somewhere in SSL stuff +### Memcheck:Value4 +### fun:MD5_Update +###} + +# +# All of these problems come from using test_socket_ssl +# +{ + from test_socket_ssl + Memcheck:Cond + fun:BN_bin2bn +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:BN_num_bits_word +} + +{ + from test_socket_ssl + Memcheck:Value4 + fun:BN_num_bits_word +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:BN_mod_exp_mont_word +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:BN_mod_exp_mont +} + +{ + from test_socket_ssl + Memcheck:Param + write(buf) + fun:write + obj:/usr/lib/libcrypto.so.0.9.7 +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:RSA_verify +} + +{ + from test_socket_ssl + Memcheck:Value4 + fun:RSA_verify +} + +{ + from test_socket_ssl + Memcheck:Value4 + fun:DES_set_key_unchecked +} + +{ + from test_socket_ssl + Memcheck:Value4 + fun:DES_encrypt2 +} + +{ + from test_socket_ssl + Memcheck:Cond + obj:/usr/lib/libssl.so.0.9.7 +} + +{ + from test_socket_ssl + Memcheck:Value4 + obj:/usr/lib/libssl.so.0.9.7 +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:BUF_MEM_grow_clean +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:memcpy + fun:ssl3_read_bytes +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:SHA1_Update +} + +{ + from test_socket_ssl + Memcheck:Value4 + fun:SHA1_Update +} + + diff -r 000000000000 -r 7ed502b17b8f Sources/OnChangeCallback.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/OnChangeCallback.cpp Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,234 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#include "OnChangeCallback.h" + +#include "PythonObject.h" + +#include + +#include + + +class PendingChange : public boost::noncopyable +{ +private: + OrthancPluginChangeType changeType_; + OrthancPluginResourceType resourceType_; + std::string resourceId_; + +public: + PendingChange(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId) : + changeType_(changeType), + resourceType_(resourceType) + { + if (resourceId == NULL) + { + resourceId_.clear(); + } + else + { + resourceId_.assign(resourceId); + } + } + + OrthancPluginChangeType GetChangeType() const + { + return changeType_; + } + + OrthancPluginResourceType GetResourceType() const + { + return resourceType_; + } + + const std::string& GetResourceId() const + { + return resourceId_; + } +}; + + + +class PendingChanges : public boost::noncopyable +{ +private: + typedef std::list Queue; + + boost::mutex mutex_; + Queue queue_; + boost::condition_variable elementAvailable_; + +public: + ~PendingChanges() + { + for (Queue::iterator it = queue_.begin(); it != queue_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + } + + void Enqueue(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId) + { + boost::mutex::scoped_lock lock(mutex_); + queue_.push_back(new PendingChange(changeType, resourceType, resourceId)); + elementAvailable_.notify_one(); + } + + PendingChange* Dequeue(unsigned int millisecondsTimeout) + { + if (millisecondsTimeout <= 0) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } + + boost::mutex::scoped_lock lock(mutex_); + + // Wait for a message to arrive in the FIFO queue + while (queue_.empty()) + { + bool success = elementAvailable_.timed_wait + (lock, boost::posix_time::milliseconds(millisecondsTimeout)); + if (!success) + { + return NULL; + } + } + + std::auto_ptr change(queue_.front()); + queue_.pop_front(); + + return change.release(); + } +}; + + + +static PendingChanges pendingChanges_; +static bool stopping_ = false; +static boost::thread changesThread_; +static PyObject* changesCallback_ = NULL; + + +static void ChangesWorker() +{ + while (!stopping_) + { + for (;;) + { + std::unique_ptr change(pendingChanges_.Dequeue(100)); + if (change.get() == NULL) + { + break; + } + else if (changesCallback_ != NULL) + { + try + { + PythonLock lock; + PythonObject args(lock, PyTuple_New(3)); + PyTuple_SetItem(args.GetPyObject(), 0, PyLong_FromLong(change->GetChangeType())); + PyTuple_SetItem(args.GetPyObject(), 1, PyLong_FromLong(change->GetResourceType())); + PyTuple_SetItem(args.GetPyObject(), 2, PyUnicode_FromString(change->GetResourceId().c_str())); + PythonObject result(lock, PyObject_CallObject(changesCallback_, args.GetPyObject())); + } + catch (OrthancPlugins::PluginException& e) + { + OrthancPlugins::LogError("Error during Python on-change callback: " + + std::string(e.What(OrthancPlugins::GetGlobalContext()))); + } + } + } + } +} + + +static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId) +{ + { + PythonLock lock; + pendingChanges_.Enqueue(changeType, resourceType, resourceId); + } + + return OrthancPluginErrorCode_Success; +} + + +PyObject* RegisterOnChangeCallback(PyObject* module, PyObject* args) +{ + // The GIL is locked at this point (no need to create "PythonLock") + + // https://docs.python.org/3/extending/extending.html#calling-python-functions-from-c + PyObject* callback = NULL; + + if (!PyArg_ParseTuple(args, "O", &callback) || + callback == NULL) + { + PyErr_SetString(PyExc_ValueError, "Expected a callback function"); + return NULL; + } + + if (changesCallback_ != NULL) + { + PyErr_SetString(PyExc_RuntimeError, "Can only register one Python on-changes callback"); + return NULL; + } + + OrthancPlugins::LogInfo("Registering a Python on-changes callback"); + + OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback); + + stopping_ = false; + changesThread_ = boost::thread(ChangesWorker); + + changesCallback_ = callback; + Py_XINCREF(changesCallback_); + + Py_INCREF(Py_None); + return Py_None; +} + + + + +void FinalizeOnChangeCallback() +{ + stopping_ = true; + + if (changesThread_.joinable()) + { + changesThread_.join(); + } + + { + PythonLock lock; + + if (changesCallback_ != NULL) + { + Py_XDECREF(changesCallback_); + } + } +} diff -r 000000000000 -r 7ed502b17b8f Sources/OnChangeCallback.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/OnChangeCallback.h Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,26 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#pragma once + +#include + +void FinalizeOnChangeCallback(); + +PyObject* RegisterOnChangeCallback(PyObject* module, PyObject* args); diff -r 000000000000 -r 7ed502b17b8f Sources/OnStoredInstanceCallback.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/OnStoredInstanceCallback.cpp Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,106 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#include "OnStoredInstanceCallback.h" + +#include "PythonObject.h" +#include "Autogenerated/sdk.h" + +#include + + +static PyObject* storedInstanceCallback_ = NULL; + + +static OrthancPluginErrorCode OnStoredInstanceCallback(OrthancPluginDicomInstance *instance, + const char *instanceId) +{ + try + { + PythonLock lock; + + /** + * Construct an instance object of the "orthanc.RestOutput" + * class. This is done by calling the constructor function + * "sdk_OrthancPluginRestOutput_Type". + **/ + PythonObject args(lock, PyTuple_New(2)); + PyTuple_SetItem(args.GetPyObject(), 0, PyLong_FromSsize_t((intptr_t) instance)); + PyTuple_SetItem(args.GetPyObject(), 1, PyBool_FromLong(true /* borrowed, don't destruct */)); + PyObject *pInst = PyObject_CallObject(GetOrthancPluginDicomInstanceType(), args.GetPyObject()); + + /** + * Construct the arguments tuple (output, uri) + **/ + PythonObject args2(lock, PyTuple_New(2)); + PyTuple_SetItem(args2.GetPyObject(), 0, pInst); + PyTuple_SetItem(args2.GetPyObject(), 1, PyUnicode_FromString(instanceId)); + + PythonObject result(lock, PyObject_CallObject(storedInstanceCallback_, args2.GetPyObject())); + return OrthancPluginErrorCode_Success; + } + catch (OrthancPlugins::PluginException& e) + { + return e.GetErrorCode(); + } +} + + +PyObject* RegisterOnStoredInstanceCallback(PyObject* module, PyObject* args) +{ + // The GIL is locked at this point (no need to create "PythonLock") + + // https://docs.python.org/3/extending/extending.html#calling-python-functions-from-c + PyObject* callback = NULL; + + if (!PyArg_ParseTuple(args, "O", &callback) || + callback == NULL) + { + PyErr_SetString(PyExc_ValueError, "Expected a callback function"); + return NULL; + } + + if (storedInstanceCallback_ != NULL) + { + PyErr_SetString(PyExc_RuntimeError, "Can only register one Python on-stored-instance callback"); + return NULL; + } + + OrthancPlugins::LogInfo("Registering a Python on-stored-instance callback"); + + OrthancPluginRegisterOnStoredInstanceCallback( + OrthancPlugins::GetGlobalContext(), OnStoredInstanceCallback); + + storedInstanceCallback_ = callback; + Py_XINCREF(storedInstanceCallback_); + + Py_INCREF(Py_None); + return Py_None; +} + + +void FinalizeOnStoredInstanceCallback() +{ + PythonLock lock; + + if (storedInstanceCallback_ != NULL) + { + Py_XDECREF(storedInstanceCallback_); + } +} diff -r 000000000000 -r 7ed502b17b8f Sources/OnStoredInstanceCallback.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/OnStoredInstanceCallback.h Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,26 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#pragma once + +#include + +PyObject* RegisterOnStoredInstanceCallback(PyObject* module, PyObject* args); + +void FinalizeOnStoredInstanceCallback(); diff -r 000000000000 -r 7ed502b17b8f Sources/Plugin.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Plugin.cpp Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,302 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +// http://edcjones.tripod.com/refcount.html +// https://docs.python.org/3/extending/extending.html + +// https://www.codevate.com/blog/7-concurrency-with-embedded-python-in-a-multi-threaded-c-application +// https://fr.slideshare.net/YiLungTsai/embed-python + + +#include "OnStoredInstanceCallback.h" +#include "OnChangeCallback.h" +#include "RestCallbacks.h" +#include "PythonModule.h" + +#include "Autogenerated/sdk.h" + +#include + +#include +#include + +#if !defined(_WIN32) +# include +#endif + + + +static bool pythonEnabled_ = false; +static std::string userScriptName_; +static std::vector globalFunctions_; + + +static void InstallClasses(PyObject* module) +{ + RegisterOrthancSdk(module); +} + + +static void SetupGlobalFunctions() +{ + if (!globalFunctions_.empty()) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + + /** + * Add all the manual global functions + **/ + + std::list functions; + + { + PyMethodDef f = { "RegisterRestCallback", RegisterRestCallback, METH_VARARGS, "" }; + functions.push_back(f); + } + + { + PyMethodDef f = { "RegisterOnChangeCallback", RegisterOnChangeCallback, METH_VARARGS, "" }; + functions.push_back(f); + } + + { + PyMethodDef f = { "RegisterOnStoredInstanceCallback", RegisterOnStoredInstanceCallback, + METH_VARARGS, "" }; + functions.push_back(f); + } + + /** + * Append all the global functions that were automatically generated + **/ + + const PyMethodDef* sdk = GetOrthancSdkFunctions(); + + for (size_t i = 0; sdk[i].ml_name != NULL; i++) + { + functions.push_back(sdk[i]); + } + + /** + * Flatten the list of functions into the vector + **/ + + globalFunctions_.resize(functions.size()); + std::copy(functions.begin(), functions.end(), globalFunctions_.begin()); + + PyMethodDef sentinel = { NULL }; + globalFunctions_.push_back(sentinel); +} + + +static PyMethodDef* GetGlobalFunctions() +{ + if (globalFunctions_.empty()) + { + // "SetupGlobalFunctions()" should have been called + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + else + { + return &globalFunctions_[0]; + } +} + + + + + +#if !defined(_WIN32) + +#include // For dl_phdr_info + +static int ForceImportCallback(struct dl_phdr_info *info, size_t size, void *data) +{ + /** + * The following line solves the error: "ImportError: + * /usr/lib/python2.7/dist-packages/PIL/_imaging.x86_64-linux-gnu.so: + * undefined symbol: PyExc_SystemError" + * https://stackoverflow.com/a/48517485/881731 + * + * dlopen("/usr/lib/x86_64-linux-gnu/libpython2.7.so", RTLD_NOW | RTLD_LAZY | RTLD_GLOBAL); + * + * Another fix consists in using LD_PRELOAD as follows: + * LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libpython2.7.so ~/Subversion/orthanc/i/Orthanc tutu.json + **/ + + std::string module(info->dlpi_name); + + if (module.find("python") != std::string::npos) + { + OrthancPlugins::LogWarning("Force global loading of Python shared library: " + module); + dlopen(module.c_str(), RTLD_NOW | RTLD_LAZY | RTLD_GLOBAL); + } + + return 0; +} + +#endif + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + OrthancPlugins::SetGlobalContext(c); + + OrthancPlugins::LogWarning("Python plugin is initializing"); + + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 0) + { + char info[1024]; + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + c->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(c, info); + return -1; + } + + + try + { + /** + * Detection of the user script + **/ + + OrthancPlugins::OrthancConfiguration config; + + static const char* const OPTION = "PythonScript"; + + std::string script; + if (!config.LookupStringValue(script, OPTION)) + { + pythonEnabled_ = false; + + OrthancPlugins::LogWarning("The option \"" + std::string(OPTION) + "\" is not provided: " + + "Python scripting is disabled"); + } + else + { + pythonEnabled_ = true; + + /** + * Installation of the user script + **/ + + const boost::filesystem::path path(script); + if (!boost::iequals(path.extension().string(), ".py")) + { + OrthancPlugins::LogError("Python script must have the \".py\" file extension: " + + path.string()); + return -1; + } + + if (!boost::filesystem::is_regular_file(path)) + { + OrthancPlugins::LogError("Inexistent directory for the Python script: " + + path.string()); + return -1; + } + + boost::filesystem::path userScriptDirectory = boost::filesystem::absolute(path).parent_path(); + + { + boost::filesystem::path module = path.filename().replace_extension(""); + userScriptName_ = module.string(); + } + + OrthancPlugins::LogWarning("Using Python script \"" + userScriptName_ + + ".py\" from directory: " + userScriptDirectory.string()); + + + /** + * Initialization of Python + **/ + +#if !defined(_WIN32) + dl_iterate_phdr(ForceImportCallback, NULL); +#endif + + SetupGlobalFunctions(); + PythonLock::GlobalInitialize("orthanc", "Exception", + GetGlobalFunctions, InstallClasses, + config.GetBooleanValue("PythonVerbose", false)); + PythonLock::AddSysPath(userScriptDirectory.string()); + + + /** + * Force loading the declarations in the user script + **/ + + PythonLock lock; + + { + PythonModule module(lock, userScriptName_); + } + + std::string traceback; + if (lock.HasErrorOccurred(traceback)) + { + OrthancPlugins::LogError("Error during the installation of the Python script, " + "traceback:\n" + traceback); + return -1; + } + } + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + OrthancPlugins::LogError("Exception while starting the Python plugin: " + + std::string(e.What(c))); + return -1; + } + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + OrthancPlugins::LogWarning("Python plugin is finalizing"); + + if (pythonEnabled_) + { + FinalizeOnChangeCallback(); + FinalizeRestCallbacks(); + FinalizeOnStoredInstanceCallback(); + + PythonLock::GlobalFinalize(); + } + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "python"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return PLUGIN_VERSION; + } +} diff -r 000000000000 -r 7ed502b17b8f Sources/PythonFunction.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/PythonFunction.cpp Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,95 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#include "PythonFunction.h" + +#include "PythonModule.h" + +#include + + +PythonObject* PythonFunction::CallUnchecked(PyObject* args) +{ + if (!IsValid()) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + else + { + PyObject* obj = PyObject_CallObject(func_->GetPyObject(), args); + return new PythonObject(lock_, obj); + } +} + + +PythonFunction::PythonFunction(PythonLock& lock, + PythonModule& module, + const std::string& name) : + lock_(lock) +{ + if (module.IsValid() && + // This check is necessary in Python 2.7, otherwise garbage collector might crash + PyObject_HasAttrString(module.GetPyObject(), name.c_str())) + { + func_.reset(module.GetObject().GetAttribute(name)); + + if (func_.get() == NULL || + !func_->IsValid() || + !PyCallable_Check(func_->GetPyObject())) + { + func_.reset(); // Not such a function + OrthancPlugins::LogWarning("Missing Python function: " + module.GetName() + + "." + name + "()"); + } + } +} + + +PythonObject* PythonFunction::Call() +{ + std::unique_ptr result(CallUnchecked(NULL)); + + std::string error; + if (lock_.HasErrorOccurred(error)) + { + OrthancPlugins::LogError("Python exception has occurred, traceback:\n" + error); + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return result.release(); + } +} + + +PythonObject* PythonFunction::Call(PythonObject& args) +{ + std::unique_ptr result(CallUnchecked(args.GetPyObject())); + + std::string error; + if (lock_.HasErrorOccurred(error)) + { + OrthancPlugins::LogError("Python exception has occurred, traceback:\n" + error); + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return result.release(); + } +} diff -r 000000000000 -r 7ed502b17b8f Sources/PythonFunction.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/PythonFunction.h Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,57 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#pragma once + +#include "../Resources/Orthanc/Core/Compatibility.h" +#include "PythonObject.h" + +#include + +class PythonModule; + +class PythonFunction : public boost::noncopyable +{ + friend class PythonLock; + +private: + PythonLock& lock_; + std::unique_ptr func_; + + PythonObject* CallUnchecked(PyObject* args); + +public: + PythonFunction(PythonLock& lock, + PythonModule& module, + const std::string& name); + + bool IsValid() const + { + return func_.get() != NULL; + } + + PythonObject* Call(); + + PythonObject* Call(PythonObject& args); + + void CallVoid() + { + std::unique_ptr result(Call()); + } +}; diff -r 000000000000 -r 7ed502b17b8f Sources/PythonLock.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/PythonLock.cpp Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,470 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#include "PythonLock.h" + +#include "PythonFunction.h" +#include "PythonModule.h" + +#include + +#include + +static boost::mutex mutex_; +static PyThreadState* interpreterState_ = NULL; +static PythonLock::ModuleFunctionsInstaller moduleFunctions_ = NULL; +static PythonLock::ModuleClassesInstaller moduleClasses_ = NULL; +static std::string moduleName_; +static std::string exceptionName_; + + +struct module_state +{ + PyObject *exceptionClass_ = NULL; +}; + +#if PY_MAJOR_VERSION >= 3 +# define GETSTATE(module) ((struct module_state*) PyModule_GetState(module)) +#else +# define GETSTATE(module) (&_state) +static struct module_state _state; +#endif + + +PythonLock::PythonLock() +{ + //OrthancPlugins::LogInfo("Python lock (GIL) acquired"); + gstate_ = PyGILState_Ensure(); +} + + +PythonLock::~PythonLock() +{ + PyGILState_Release(gstate_); + //OrthancPlugins::LogInfo("Python lock (GIL) released"); +} + + +void PythonLock::ExecuteCommand(const std::string& s) +{ + if (PyRun_SimpleString(s.c_str()) != 0) + { + OrthancPlugins::LogError("Error while executing a Python command"); + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } +} + + +void PythonLock::ExecuteFile(const std::string& path) +{ + OrthancPlugins::MemoryBuffer buffer; + buffer.ReadFile(path); + + std::string script; + buffer.ToString(script); + + ExecuteCommand(script); +} + + +bool PythonLock::HasErrorOccurred(std::string& target) +{ + if (PyErr_Occurred()) + { + PyObject *exceptionType = NULL; + PyObject *exceptionValue = NULL; + PyObject *traceback = NULL; + PyErr_Fetch(&exceptionType, &exceptionValue, &traceback); + + if (exceptionType == NULL) + { + return false; + } + + PyErr_NormalizeException(&exceptionType, &exceptionValue, &traceback); + +#if PY_MAJOR_VERSION >= 3 + if (traceback != NULL) + { + PyException_SetTraceback(exceptionValue, traceback); + } +#endif + + if (exceptionType != NULL) + { + PythonObject temp(*this, PyObject_Str(exceptionType)); + std::string s; + if (temp.ToUtf8String(s)) + { + target += s + "\n"; + } + } + + if (exceptionValue != NULL) + { + PythonObject temp(*this, PyObject_Str(exceptionValue)); + std::string s; + if (temp.ToUtf8String(s)) + { + target += s + "\n"; + } + } + + { + PythonModule module(*this, "traceback"); + PythonFunction f(*this, module, "format_tb"); + + if (traceback != NULL && + f.IsValid()) + { + PythonObject args(*this, PyTuple_New(1)); + PyTuple_SetItem(args.GetPyObject(), 0, traceback); + + std::auto_ptr value(f.CallUnchecked(args.GetPyObject())); + + if (value->IsValid()) + { + Py_ssize_t len = PyList_Size(value->GetPyObject()); + for (Py_ssize_t i = 0; i < len; i++) + { + PythonObject item(*this, PyList_GetItem(value->GetPyObject(), i), true /* borrowed */); + std::string line; + if (item.ToUtf8String(line)) + { + target += "\n" + line; + } + } + } + } + } + + + /** + * "This call takes away a reference to each object: you must own + * a reference to each object before the call and after the call + * you no longer own these references. (If you don't understand + * this, don't use this function. I warned you.)" + * => I don't use PyErr_Restore() + **/ + + //PyErr_Restore(exceptionType, exceptionValue, traceback); + //PyErr_Clear(); + + return true; + } + else + { + return false; + } +} + + +static void RegisterException(PyObject* module, + const std::string& name) +{ + struct module_state *state = GETSTATE(module); + + state->exceptionClass_ = PyErr_NewException(const_cast(name.c_str()), NULL, NULL); + if (state->exceptionClass_ == NULL) + { + Py_DECREF(module); + OrthancPlugins::LogError("Cannot create the Python exception class"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + +#if 1 + Py_XINCREF(state->exceptionClass_); + if (PyModule_AddObject(module, "Exception", state->exceptionClass_) < 0) + { + Py_XDECREF(state->exceptionClass_); + Py_CLEAR(state->exceptionClass_); + OrthancPlugins::LogError("Cannot create the Python exception class"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } +#else + // Install the exception class + PyObject* dict = PyModule_GetDict(module); + PyDict_SetItemString(dict, "Exception", state->exceptionClass); +#endif +} + + + +#if PY_MAJOR_VERSION >= 3 + +static int sdk_traverse(PyObject *module, visitproc visit, void *arg) +{ + Py_VISIT(GETSTATE(module)->exceptionClass_); + return 0; +} + +static int sdk_clear(PyObject *module) +{ + Py_CLEAR(GETSTATE(module)->exceptionClass_); + return 0; +} + +static struct PyModuleDef moduledef = +{ + PyModuleDef_HEAD_INIT, + NULL, /* m_name => TO BE FILLED */ + NULL, + sizeof(struct module_state), + NULL, /* m_methods => TO BE FILLED */ + NULL, + sdk_traverse, + sdk_clear, + NULL +}; + + +PyMODINIT_FUNC InitializeModule() +{ + if (moduleFunctions_ == NULL || + moduleClasses_ == NULL || + moduleName_.empty() || + exceptionName_.empty()) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + moduledef.m_name = moduleName_.c_str(); + moduledef.m_methods = moduleFunctions_(); + + PyObject *module = PyModule_Create(&moduledef); + if (module == NULL) + { + OrthancPlugins::LogError("Cannot create a Python module"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + RegisterException(module, moduleName_ + "." + exceptionName_); + moduleClasses_(module); + + return module; +} + +#else + +void InitializeModule() +{ + if (moduleFunctions_ == NULL || + moduleClasses_ == NULL || + moduleName_.empty() || + exceptionName_.empty()) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + PyObject *module = Py_InitModule(moduleName_.c_str(), moduleFunctions_()); + if (module == NULL) + { + OrthancPlugins::LogError("Cannot create a Python module"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + RegisterException(module, moduleName_ + "." + exceptionName_); + moduleClasses_(module); +} + +#endif + + + +void PythonLock::GlobalInitialize(const std::string& moduleName, + const std::string& exceptionName, + ModuleFunctionsInstaller moduleFunctions, + ModuleClassesInstaller moduleClasses, + bool verbose) +{ + boost::mutex::scoped_lock lock(mutex_); + + if (interpreterState_ != NULL) + { + OrthancPlugins::LogError("Cannot initialize twice the Python interpreter"); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + + if (moduleClasses == NULL || + moduleFunctions == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer); + } + + if (moduleName.empty() || + exceptionName.empty()) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } + + if (exceptionName.find('.') != std::string::npos) + { + OrthancPlugins::LogError("The name of the exception cannot contain \".\", found: " + + exceptionName); + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } + + moduleClasses_ = moduleClasses; + moduleFunctions_ = moduleFunctions; + moduleName_ = moduleName; + exceptionName_ = exceptionName; + + std::string executable; + + { + OrthancPlugins::OrthancString str; + str.Assign(OrthancPluginGetOrthancPath(OrthancPlugins::GetGlobalContext())); + str.ToString(executable); + } + + OrthancPlugins::LogWarning("Program name: " + executable); + +#if PY_MAJOR_VERSION == 2 + Py_SetProgramName(&executable[0]); /* optional but recommended */ +#else + std::wstring wide(executable.begin(), executable.end()); + Py_SetProgramName(&wide[0]); /* optional but recommended */ + Py_UnbufferedStdioFlag = 1; // Write immediately to stdout after "sys.stdout.flush()" (only in Python 3.x) +#endif + + Py_InspectFlag = 1; // Don't exit the Orthanc process on Python error + + if (verbose) + { + Py_VerboseFlag = 1; + } + +#if PY_MAJOR_VERSION >= 3 + PyImport_AppendInittab(moduleName_.c_str(), InitializeModule); +#endif + +#if PY_VERSION_HEX < 0x03020000 /* 3.2.0 */ + /** + * "Changed in version 3.2: This function cannot be called before + * Py_Initialize() anymore." + **/ + if (!PyEval_ThreadsInitialized()) + { + PyEval_InitThreads(); + } +#endif + + Py_InitializeEx(0 /* Python is embedded, skip signal handlers */); + +#if PY_MAJOR_VERSION == 2 + std::cout << Py_GetPath() << std::endl; + std::cout << Py_GetProgramName() << std::endl; + std::cout << Py_GetProgramFullPath() << std::endl; +#else + std::wcout << Py_GetPath() << std::endl; + std::wcout << Py_GetProgramName() << std::endl; + std::wcout << Py_GetProgramFullPath() << std::endl; +#endif + + +#if (PY_VERSION_HEX >= 0x03020000 /* 3.2.0 */ && \ + PY_VERSION_HEX < 0x03070000 /* 3.7.0 */) + /** + * Changed in version 3.7: This function is now called by + * Py_Initialize(), so you don't have to call it yourself anymore. + **/ + if (!PyEval_ThreadsInitialized()) + { + PyEval_InitThreads(); + } +#endif + + +#if PY_MAJOR_VERSION == 2 + InitializeModule(); +#endif + + // https://fr.slideshare.net/YiLungTsai/embed-python => slide 26 + interpreterState_ = PyEval_SaveThread(); +} + + +void PythonLock::AddSysPath(const std::string& path) +{ + /** + * "It is recommended that applications embedding the Python + * interpreter for purposes other than executing a single script + * pass 0 as updatepath, and update sys.path themselves if + * desired. See CVE-2008-5983." + * => Set "sys.path.append()" to the directory of the configuration file by default + **/ + + PythonLock lock; + + /** + * Initialization of "sys.path.append()" must be done before loading + * any module. + **/ + + PyObject *sysPath = PySys_GetObject(const_cast("path")); + if (sysPath == NULL) + { + OrthancPlugins::LogError("Cannot find sys.path"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + PyObject* pyPath = PyUnicode_FromString(path.c_str()); + int result = PyList_Insert(sysPath, 0, pyPath); + Py_DECREF(pyPath); + + if (result != 0) + { + OrthancPlugins::LogError("Cannot run sys.path.append()"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } +} + + +void PythonLock::GlobalFinalize() +{ + boost::mutex::scoped_lock lock(mutex_); + + if (interpreterState_ != NULL) + { + PyEval_RestoreThread(interpreterState_); + interpreterState_ = NULL; + } + + Py_Finalize(); +} + + +void PythonLock::RaiseException(PyObject* module, + OrthancPluginErrorCode code) +{ + if (code != OrthancPluginErrorCode_Success) + { + const char* message = OrthancPluginGetErrorDescription(OrthancPlugins::GetGlobalContext(), code); + + struct module_state *state = GETSTATE(module); + if (state->exceptionClass_ == NULL) + { + OrthancPlugins::LogError("No Python exception has been registered"); + } + else + { + PyErr_SetString(state->exceptionClass_, message); + } + } +} diff -r 000000000000 -r 7ed502b17b8f Sources/PythonLock.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/PythonLock.h Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,62 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#pragma once + +#include + +#define PY_SSIZE_T_CLEAN /* Make "s#" use Py_ssize_t rather than int. */ +#include + +#include +#include + +class PythonLock : public boost::noncopyable +{ +private: + PyGILState_STATE gstate_; + +public: + typedef void (*ModuleClassesInstaller) (PyObject* module); + + typedef PyMethodDef* (*ModuleFunctionsInstaller) (); + + PythonLock(); + + ~PythonLock(); + + void ExecuteCommand(const std::string& s); + + void ExecuteFile(const std::string& path); + + bool HasErrorOccurred(std::string& traceback); + + static void GlobalInitialize(const std::string& moduleName, + const std::string& exceptionName, + ModuleFunctionsInstaller moduleFunctions, + ModuleClassesInstaller moduleClasses, + bool verbose); + + static void GlobalFinalize(); + + static void AddSysPath(const std::string& path); + + static void RaiseException(PyObject* module, + OrthancPluginErrorCode code); +}; diff -r 000000000000 -r 7ed502b17b8f Sources/PythonModule.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/PythonModule.cpp Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,116 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#include "PythonModule.h" + +#include "PythonString.h" + +#include + +#include +#include + + +#if PY_VERSION_HEX < 0x03070000 /* 3.7.0 */ +static PythonObject* GetModule(PythonLock& lock, + const std::string& utf8Name) +{ + static boost::mutex mutex_; + static std::set loadedModules_; + + boost::mutex::scoped_lock l(mutex_); + + if (loadedModules_.find(utf8Name) == loadedModules_.end()) + { + PythonString tmp(lock, utf8Name); + + // The module is not imported yet + loadedModules_.insert(utf8Name); + return new PythonObject(lock, PyImport_Import(tmp.GetPyObject()), false); + } + else + { + // PyImport_AddModule returns a borrowed reference + return new PythonObject(lock, PyImport_AddModule(utf8Name.c_str()), true); + } +} + +#else +// The "PyImport_GetModule()" function was introduced in Python +// 3.7.0 +static PythonObject* GetModule(PythonLock& lock, + const std::string& utf8Name) +{ + PythonString tmp(lock, utf8Name); + + PyObject* module = PyImport_GetModule(tmp.GetPyObject()); + if (module != NULL) + { + // The module was already imported by a previous call: Reuse it + return new PythonObject(lock, module); + } + else + { + // This is the first time this module is used: Import it + return new PythonObject(lock, PyImport_Import(tmp.GetPyObject())); + } +} +#endif + + +PythonModule::PythonModule(PythonLock& lock, + const std::string& utf8Name) : + lock_(lock), + name_(utf8Name) +{ + module_.reset(GetModule(lock, utf8Name)); +} + + +bool PythonModule::IsValid() const +{ + return (module_.get() != NULL && + module_->IsValid()); +} + + +PythonObject& PythonModule::GetObject() const +{ + if (!IsValid()) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + else + { + return *module_; + } +} + + +PyObject* PythonModule::GetPyObject() const +{ + if (!IsValid()) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + else + { + return module_->GetPyObject(); + } +} diff -r 000000000000 -r 7ed502b17b8f Sources/PythonModule.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/PythonModule.h Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,48 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#pragma once + +#include "../Resources/Orthanc/Core/Compatibility.h" +#include "PythonObject.h" + +#include + +class PythonModule : public boost::noncopyable +{ +private: + PythonLock& lock_; + std::string name_; + std::unique_ptr module_; + +public: + PythonModule(PythonLock& lock, + const std::string& utf8Name); + + const std::string& GetName() const + { + return name_; + } + + bool IsValid() const; + + PythonObject& GetObject() const; + + PyObject* GetPyObject() const; +}; diff -r 000000000000 -r 7ed502b17b8f Sources/PythonObject.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/PythonObject.cpp Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,134 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#include "PythonObject.h" + + +#include "PythonLock.h" + +#include + + +PythonObject::PythonObject(PythonLock& lock, + PyObject *object, + bool borrowed) : + lock_(lock), + object_(object), + borrowed_(borrowed) +{ +} + + +PythonObject::~PythonObject() +{ + if (!borrowed_ && + object_ != NULL) + { + Py_DECREF(object_); + } +} + + +PyObject* PythonObject::GetPyObject() const +{ + if (object_ == NULL) + { + // "IsValid()" should have been called + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + else + { + return object_; + } +} + + +PythonObject* PythonObject::GetAttribute(const std::string& name) +{ + return new PythonObject(lock_, PyObject_GetAttrString(GetPyObject(), name.c_str())); +} + + +bool PythonObject::ToUtf8String(std::string& target) +{ + PyObject* value = GetPyObject(); // Raises an exception if needed + + if (PyUnicode_Check(value)) + { + PythonObject encoded(lock_, PyUnicode_AsEncodedString(value, "utf-8", "replace")); + if (encoded.IsValid()) + { + target = PyBytes_AS_STRING(encoded.GetPyObject()); + return true; + } + else + { + target.clear(); + return false; + } + } +#if PY_MAJOR_VERSION == 2 + else if (PyString_Check(value)) + { + target = PyString_AS_STRING(value); + return true; + } +#endif + else + { + target.clear(); + return false; + } +} + + +void PythonObject::Format(std::ostream& os) +{ + std::string s; + if (object_ == NULL) + { + os << "Can't format a NULL Python object" << std::endl; + } + else if (ToUtf8String(s)) + { + os << s; + } + else + { + os << "Can't format this Python object" << std::endl; + } +} + + +PyObject* PythonObject::Release() +{ + if (!borrowed_ && + object_ != NULL) + { + PyObject* value = object_; + object_ = NULL; + return value; + } + else + { + OrthancPlugins::LogError("Cannot release a NULL or borrowed reference"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } +} + diff -r 000000000000 -r 7ed502b17b8f Sources/PythonObject.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/PythonObject.h Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,52 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#pragma once + +#include "PythonLock.h" + +class PythonObject : public boost::noncopyable +{ +private: + PythonLock& lock_; + PyObject *object_; + bool borrowed_; + +public: + PythonObject(PythonLock& lock, + PyObject *object, + bool borrowed = false); + + ~PythonObject(); + + bool IsValid() const + { + return object_ != NULL; + } + + PyObject* GetPyObject() const; + + PythonObject* GetAttribute(const std::string& name); + + bool ToUtf8String(std::string& target); + + void Format(std::ostream& os); + + PyObject* Release(); +}; diff -r 000000000000 -r 7ed502b17b8f Sources/PythonString.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/PythonString.cpp Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,33 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#include "PythonString.h" + +#include + +PythonString::PythonString(PythonLock& lock, + const std::string& utf8) : + string_(lock, PyUnicode_FromString(utf8.c_str())) +{ + if (!string_.IsValid()) + { + OrthancPlugins::LogError("Cannot create a Python string"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } +} diff -r 000000000000 -r 7ed502b17b8f Sources/PythonString.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/PythonString.h Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,38 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#pragma once + +#include "PythonObject.h" + +// A Python string is always valid, or an exception was thrown on its creation +class PythonString : public boost::noncopyable +{ +private: + PythonObject string_; + +public: + PythonString(PythonLock& lock, + const std::string& utf8); + + PyObject* GetPyObject() const + { + return string_.GetPyObject(); + } +}; diff -r 000000000000 -r 7ed502b17b8f Sources/RestCallbacks.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/RestCallbacks.cpp Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,232 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#include "RestCallbacks.h" + +#include "PythonObject.h" +#include "Autogenerated/sdk.h" + +#include + +#include + + +class RestCallback : public boost::noncopyable +{ +private: + boost::regex regex_; + PyObject* callback_; + +public: + RestCallback(const std::string& uri, + PyObject* callback) : + regex_(uri), + callback_(callback) + { + Py_XINCREF(callback_); + } + + ~RestCallback() + { + Py_XDECREF(callback_); + } + + bool IsMatch(const std::string& uri) const + { + return boost::regex_match(uri, regex_); + } + + PyObject* GetCallback() + { + return callback_; + } +}; + + +// Concurrent accesses to the callbacks are protected by the +// "PythonLock" (GIL mutex) +static std::list restCallbacks_; + + +static void RestCallbackHandler(OrthancPluginRestOutput* output, + const char* uri, + const OrthancPluginHttpRequest* request) +{ + PythonLock lock; + + for (std::list::const_iterator it = restCallbacks_.begin(); + it != restCallbacks_.end(); ++it) + { + assert(*it != NULL); + if ((*it)->IsMatch(uri)) + { + /** + * Construct an instance object of the "orthanc.RestOutput" + * class. This is done by calling the constructor function + * "sdk_OrthancPluginRestOutput_Type". + **/ + PythonObject args(lock, PyTuple_New(2)); + PyTuple_SetItem(args.GetPyObject(), 0, PyLong_FromSsize_t((intptr_t) output)); + PyTuple_SetItem(args.GetPyObject(), 1, PyBool_FromLong(true /* borrowed, don't destruct */)); + PyObject *pInst = PyObject_CallObject(GetOrthancPluginRestOutputType(), args.GetPyObject()); + + + /** + * Construct the arguments tuple (output, uri) + **/ + PythonObject args2(lock, PyTuple_New(2)); + PyTuple_SetItem(args2.GetPyObject(), 0, pInst); + PyTuple_SetItem(args2.GetPyObject(), 1, PyUnicode_FromString(uri)); + // No need to decrement refcount with "PyTuple_SetItem()" + + + /** + * Construct the named arguments from the "request" argument + **/ + const char* method; + switch (request->method) + { + case OrthancPluginHttpMethod_Get: + method = "GET"; + break; + + case OrthancPluginHttpMethod_Post: + method = "POST"; + break; + + case OrthancPluginHttpMethod_Put: + method = "PUT"; + break; + + case OrthancPluginHttpMethod_Delete: + method = "DELETE"; + break; + + default: + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } + + PythonObject kw(lock, PyDict_New()); + PyDict_SetItemString(kw.GetPyObject(), "method", PyUnicode_FromString(method)); + + { + PythonObject groups(lock, PyTuple_New(request->groupsCount)); + + for (uint32_t i = 0; i < request->groupsCount; i++) + { + PyTuple_SetItem(groups.GetPyObject(), i, PyUnicode_FromString(request->groups[i])); + } + + PyDict_SetItemString(kw.GetPyObject(), "groups", groups.Release()); + } + + if (request->method == OrthancPluginHttpMethod_Get) + { + PythonObject get(lock, PyDict_New()); + + OrthancPlugins::LogError(boost::lexical_cast(request->getCount)); + + for (uint32_t i = 0; i < request->getCount; i++) + { + PyDict_SetItemString(get.GetPyObject(), request->getKeys[i], + PyUnicode_FromString(request->getValues[i])); + } + + PyDict_SetItemString(kw.GetPyObject(), "get", get.Release()); + } + + { + PythonObject headers(lock, PyDict_New()); + + OrthancPlugins::LogError(boost::lexical_cast(request->headersCount)); + + for (uint32_t i = 0; i < request->headersCount; i++) + { + PyDict_SetItemString(headers.GetPyObject(), request->headersKeys[i], + PyUnicode_FromString(request->headersValues[i])); + } + + PyDict_SetItemString(kw.GetPyObject(), "headers", headers.Release()); + } + + if (request->method == OrthancPluginHttpMethod_Post || + request->method == OrthancPluginHttpMethod_Put) + { + PyDict_SetItemString(kw.GetPyObject(), "body", PyBytes_FromStringAndSize( + reinterpret_cast(request->body), request->bodySize)); + } + + + /** + * Call the user-defined function + **/ + PythonObject result(lock, PyObject_Call( + (*it)->GetCallback(), args2.GetPyObject(), kw.GetPyObject())); + + return; + } + } + + // Should never happen + OrthancPlugins::LogError("Unable to find the Python handler for URI: " + std::string(uri)); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); +} + + + +PyObject* RegisterRestCallback(PyObject* module, PyObject* args) +{ + // The GIL is locked at this point (no need to create "PythonLock") + + // https://docs.python.org/3/extending/extending.html#calling-python-functions-from-c + const char* uri = NULL; + PyObject* callback = NULL; + + if (!PyArg_ParseTuple(args, "sO", &uri, &callback) || + uri == NULL || + callback == NULL) + { + PyErr_SetString(PyExc_ValueError, "Expected a string (URI) and a callback function"); + return NULL; + } + + OrthancPlugins::LogInfo("Registering a Python REST callback on URI: " + std::string(uri)); + OrthancPlugins::RegisterRestCallback(uri, true /* thread safe */); + + restCallbacks_.push_back(new RestCallback(uri, callback)); + + Py_INCREF(Py_None); + return Py_None; +} + + + +void FinalizeRestCallbacks() +{ + PythonLock lock; + + for (std::list::iterator it = restCallbacks_.begin(); + it != restCallbacks_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + + restCallbacks_.clear(); +} diff -r 000000000000 -r 7ed502b17b8f Sources/RestCallbacks.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/RestCallbacks.h Thu Mar 26 18:47:01 2020 +0100 @@ -0,0 +1,26 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * 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 . + **/ + + +#pragma once + +#include + +PyObject* RegisterRestCallback(PyObject* module, PyObject* args); + +void FinalizeRestCallbacks();